Skip to content

Commit d666f5f

Browse files
committed
stop storing RegionWrappedCEK and RegionKeyVersion in DB
We will store keys in OpenBao. Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
1 parent f83f9c0 commit d666f5f

7 files changed

Lines changed: 38 additions & 279 deletions

File tree

inmem/stores.go

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -172,23 +172,6 @@ func (m *MemStore) GetEncryptionParams(_ context.Context, space did.DID, digest
172172
return &cp, nil
173173
}
174174

175-
func (m *MemStore) RewrapEncryptionParams(_ context.Context, space did.DID, digest, wrappedCEK []byte, keyVersion string) error {
176-
if err := registry.ValidateRewrap(wrappedCEK, keyVersion); err != nil {
177-
return err
178-
}
179-
m.mu.Lock()
180-
defer m.mu.Unlock()
181-
key := locKey{space, string(digest)}
182-
params, ok := m.encParams[key]
183-
if !ok {
184-
return registry.ErrNotFound
185-
}
186-
params.RegionWrappedCEK = cloneBytes(wrappedCEK)
187-
params.RegionKeyVersion = keyVersion
188-
m.encParams[key] = params
189-
return nil
190-
}
191-
192175
func (m *MemStore) DeleteEncryptionParams(_ context.Context, space did.DID, digest []byte) error {
193176
m.mu.Lock()
194177
defer m.mu.Unlock()
@@ -439,12 +422,10 @@ func cloneLocation(loc registry.BlobLocation) registry.BlobLocation {
439422
return loc
440423
}
441424

442-
// cloneEncryptionParams deep-copies a BlobEncryptionParams' byte-slice fields —
443-
// the digest and the key material — so the stored copy and any returned copy
444-
// never alias the caller's slices.
425+
// cloneEncryptionParams deep-copies a BlobEncryptionParams' byte-slice fields
426+
// so the stored copy and any returned copy never alias the caller's slices.
445427
func cloneEncryptionParams(p registry.BlobEncryptionParams) registry.BlobEncryptionParams {
446428
p.Digest = cloneBytes(p.Digest)
447-
p.RegionWrappedCEK = cloneBytes(p.RegionWrappedCEK)
448429
p.BaseNonce = cloneBytes(p.BaseNonce)
449430
p.AAD = cloneBytes(p.AAD)
450431
return p

inmem/stores_test.go

Lines changed: 3 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,6 @@ func feeParams(space did.DID, digest []byte) registry.BlobEncryptionParams {
261261
return registry.BlobEncryptionParams{
262262
Space: space,
263263
Digest: digest,
264-
RegionWrappedCEK: []byte("wrapped-cek"),
265-
RegionKeyVersion: "region-v1",
266264
TenantRecipientKID: "did:key:tenant#wrap",
267265
HeaderLen: 212,
268266
BaseNonce: []byte("nonce07"),
@@ -302,7 +300,7 @@ func TestEncryptionParams_MissingIsNotFound(t *testing.T) {
302300
}
303301
}
304302

305-
func TestEncryptionParams_DeleteShreds(t *testing.T) {
303+
func TestEncryptionParams_DeleteRemovesRow(t *testing.T) {
306304
ctx := context.Background()
307305
m := NewMemStore()
308306
space := testutil.RandomDID(t)
@@ -328,7 +326,7 @@ func TestEncryptionParams_DeleteIsIdempotent(t *testing.T) {
328326
}
329327
}
330328

331-
// The store must not alias the caller's key material, nor let a caller reach
329+
// The store must not alias the caller's byte slices, nor let a caller reach
332330
// back into it through a returned copy.
333331
func TestEncryptionParams_NoSliceAliasing(t *testing.T) {
334332
ctx := context.Background()
@@ -340,15 +338,14 @@ func TestEncryptionParams_NoSliceAliasing(t *testing.T) {
340338
if err := m.PutEncryptionParams(ctx, params); err != nil {
341339
t.Fatalf("PutEncryptionParams: %v", err)
342340
}
343-
params.RegionWrappedCEK[0] = 'X'
344341
params.BaseNonce[0] = 'X'
345342
params.AAD[0] = 'X'
346343

347344
got, err := m.GetEncryptionParams(ctx, space, digest)
348345
if err != nil {
349346
t.Fatalf("GetEncryptionParams: %v", err)
350347
}
351-
got.RegionWrappedCEK[0] = 'Y'
348+
got.BaseNonce[0] = 'Y'
352349

353350
again, err := m.GetEncryptionParams(ctx, space, digest)
354351
if err != nil {
@@ -359,105 +356,6 @@ func TestEncryptionParams_NoSliceAliasing(t *testing.T) {
359356
}
360357
}
361358

362-
// A region-key rotation re-wraps in place: same blob, new CEK + key version,
363-
// every other parameter untouched.
364-
func TestEncryptionParams_RewrapInPlace(t *testing.T) {
365-
ctx := context.Background()
366-
m := NewMemStore()
367-
space := testutil.RandomDID(t)
368-
digest := []byte("enc-digest")
369-
if err := m.PutEncryptionParams(ctx, feeParams(space, digest)); err != nil {
370-
t.Fatalf("PutEncryptionParams: %v", err)
371-
}
372-
373-
want := feeParams(space, digest)
374-
want.RegionWrappedCEK = []byte("wrapped-cek-v2")
375-
want.RegionKeyVersion = "region-v2"
376-
if err := m.RewrapEncryptionParams(ctx, space, digest, want.RegionWrappedCEK, want.RegionKeyVersion); err != nil {
377-
t.Fatalf("RewrapEncryptionParams: %v", err)
378-
}
379-
380-
got, err := m.GetEncryptionParams(ctx, space, digest)
381-
if err != nil {
382-
t.Fatalf("GetEncryptionParams: %v", err)
383-
}
384-
if !reflect.DeepEqual(*got, want) {
385-
t.Fatalf("after re-wrap = %+v, want %+v", *got, want)
386-
}
387-
}
388-
389-
// Nothing to re-wrap means the blob is not encrypted (or was already shredded),
390-
// which a rotation must hear about rather than take for success.
391-
func TestEncryptionParams_RewrapMissingIsNotFound(t *testing.T) {
392-
ctx := context.Background()
393-
m := NewMemStore()
394-
395-
err := m.RewrapEncryptionParams(ctx, testutil.RandomDID(t), []byte("absent"), []byte("wrapped-cek-v2"), "region-v2")
396-
if !errors.Is(err, registry.ErrNotFound) {
397-
t.Fatalf("RewrapEncryptionParams(absent) err = %v, want ErrNotFound", err)
398-
}
399-
}
400-
401-
// A re-wrap may not blank out the key material the decrypt path needs.
402-
func TestEncryptionParams_RewrapIncompleteRejected(t *testing.T) {
403-
space := testutil.RandomDID(t)
404-
digest := []byte("enc-digest")
405-
406-
cases := map[string]struct {
407-
wrappedCEK []byte
408-
keyVersion string
409-
}{
410-
"no wrapped CEK": {nil, "region-v2"},
411-
"empty wrapped CEK": {[]byte{}, "region-v2"},
412-
"no key version": {[]byte("wrapped-cek-v2"), ""},
413-
}
414-
for name, tc := range cases {
415-
t.Run(name, func(t *testing.T) {
416-
ctx := context.Background()
417-
m := NewMemStore()
418-
if err := m.PutEncryptionParams(ctx, feeParams(space, digest)); err != nil {
419-
t.Fatalf("PutEncryptionParams: %v", err)
420-
}
421-
422-
err := m.RewrapEncryptionParams(ctx, space, digest, tc.wrappedCEK, tc.keyVersion)
423-
if !errors.Is(err, registry.ErrInvalidEncryptionParams) {
424-
t.Fatalf("RewrapEncryptionParams err = %v, want ErrInvalidEncryptionParams", err)
425-
}
426-
got, getErr := m.GetEncryptionParams(ctx, space, digest)
427-
if getErr != nil {
428-
t.Fatalf("GetEncryptionParams: %v", getErr)
429-
}
430-
if !reflect.DeepEqual(*got, feeParams(space, digest)) {
431-
t.Fatalf("rejected re-wrap altered the row: %+v", *got)
432-
}
433-
})
434-
}
435-
}
436-
437-
// The re-wrap path must not alias the caller's key material either.
438-
func TestEncryptionParams_RewrapNoSliceAliasing(t *testing.T) {
439-
ctx := context.Background()
440-
m := NewMemStore()
441-
space := testutil.RandomDID(t)
442-
digest := []byte("enc-digest")
443-
if err := m.PutEncryptionParams(ctx, feeParams(space, digest)); err != nil {
444-
t.Fatalf("PutEncryptionParams: %v", err)
445-
}
446-
wrappedCEK := []byte("wrapped-cek-v2")
447-
if err := m.RewrapEncryptionParams(ctx, space, digest, wrappedCEK, "region-v2"); err != nil {
448-
t.Fatalf("RewrapEncryptionParams: %v", err)
449-
}
450-
wrappedCEK[0] = 'X'
451-
452-
got, err := m.GetEncryptionParams(ctx, space, digest)
453-
if err != nil {
454-
t.Fatalf("GetEncryptionParams: %v", err)
455-
}
456-
if !reflect.DeepEqual(got.RegionWrappedCEK, []byte("wrapped-cek-v2")) {
457-
t.Fatalf("re-wrapped CEK was aliased: %q", got.RegionWrappedCEK)
458-
}
459-
}
460-
461359
// Every parameter is required: a row missing any one of them could not be
462360
// decrypted with, so PutEncryptionParams rejects it and stores nothing.
463361
func TestEncryptionParams_IncompleteRejected(t *testing.T) {
@@ -470,9 +368,6 @@ func TestEncryptionParams_IncompleteRejected(t *testing.T) {
470368
}{
471369
{"no space", func(p *registry.BlobEncryptionParams) { p.Space = did.Undef }},
472370
{"no digest", func(p *registry.BlobEncryptionParams) { p.Digest = nil }},
473-
{"no wrapped CEK", func(p *registry.BlobEncryptionParams) { p.RegionWrappedCEK = nil }},
474-
{"empty wrapped CEK", func(p *registry.BlobEncryptionParams) { p.RegionWrappedCEK = []byte{} }},
475-
{"no key version", func(p *registry.BlobEncryptionParams) { p.RegionKeyVersion = "" }},
476371
{"no recipient kid", func(p *registry.BlobEncryptionParams) { p.TenantRecipientKID = "" }},
477372
{"no header length", func(p *registry.BlobEncryptionParams) { p.HeaderLen = 0 }},
478373
{"no base nonce", func(p *registry.BlobEncryptionParams) { p.BaseNonce = nil }},

migrations/sql/00013_blob_encryption.sql

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
-- COSE/STREAM ciphertext envelope; a range GET must be able to decrypt any byte
55
-- span of that envelope WITHOUT first fetching and parsing its header. A row
66
-- here caches exactly the inputs the read path's decryptor needs, so a read
7-
-- unwraps the CEK (under the region KEK) and goes straight to a body-range
8-
-- fetch — no envelope-header round-trip.
7+
-- unwraps the CEK (held in OpenBao, under the region KEK) and goes straight to
8+
-- a body-range fetch — no envelope-header round-trip.
99
--
1010
-- The existence of a row is what marks a blob as encrypted, so every column is
1111
-- NOT NULL: there is no such thing as a half-populated parameter set the
@@ -15,29 +15,22 @@
1515
-- WITHOUT a foreign key to it. blob_locations is a reconstructible cache of the
1616
-- indexing-service contract — every row can be re-derived from the indexer or
1717
-- the accept receipt, and the table goes away when the topology moves to a real
18-
-- indexer. A wrapped CEK is not reconstructible: lose the row and the
19-
-- ciphertext is permanently unreadable. The two therefore have independent
20-
-- lifecycles, and an FK would additionally force location-before-parameters
21-
-- write ordering.
18+
-- indexer. A row here is instead the marker that a blob is encrypted, so the
19+
-- two have independent lifecycles, and an FK would additionally force
20+
-- location-before-parameters write ordering.
2221
--
2322
-- aad holds the whole COSE Enc_structure rather than just the protected header,
2423
-- because the structure's context string differs between a COSE_Encrypt and a
2524
-- COSE_Encrypt0 and a bare row cannot record which form was used. The protected
2625
-- header stays recoverable from it as element 1.
2726
--
28-
-- Raw CEK bytes are never stored — only the CEK wrapped under the region KEK
29-
-- (region_wrapped_cek) plus the opaque key-version / recipient identifiers
30-
-- needed to unwrap it. Per-blob crypto-shred is deleting the row; because there
31-
-- is no cascade, a caller removing a blob must delete here as well as from
32-
-- blob_locations. Re-wrap under a rotated region key is an upsert that replaces
33-
-- region_wrapped_cek and region_key_version in place.
27+
-- No key material is stored here: the region-KEK-wrapped CEK and its key
28+
-- version live in OpenBao. Per-blob crypto-shred is deleting the key there;
29+
-- deleting this row only drops the cached decrypt parameters. Because there is no cascade, a caller removing a
30+
-- blob must delete here as well as from blob_locations.
3431
CREATE TABLE ingot.blob_encryption_params (
3532
space text NOT NULL,
3633
digest bytea NOT NULL, -- ciphertext blob multihash, as in blob_locations
37-
region_wrapped_cek bytea NOT NULL -- CEK wrapped under the region KEK (A256KW)
38-
CHECK (octet_length(region_wrapped_cek) > 0),
39-
region_key_version text NOT NULL -- opaque id of the region KEK version used (rotation re-wrap)
40-
CHECK (region_key_version <> ''),
4134
tenant_recipient_kid text NOT NULL -- opaque id of the Hilt wrap key (insurance-recovery unwrap)
4235
CHECK (tenant_recipient_kid <> ''),
4336
header_len bigint NOT NULL -- encoded envelope length; the ciphertext starts at this offset

migrations/up_live_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,8 @@ func TestUp_Live(t *testing.T) {
7676
// presence of a row is what marks a blob as encrypted, so there is no such
7777
// thing as a half-populated parameter set.
7878
for _, col := range []string{
79-
"space", "digest", "region_wrapped_cek", "region_key_version",
80-
"tenant_recipient_kid", "header_len", "base_nonce", "chunk_size", "aad",
81-
"created_at",
79+
"space", "digest", "tenant_recipient_kid", "header_len", "base_nonce",
80+
"chunk_size", "aad", "created_at",
8281
} {
8382
var nullable string
8483
err := pool.QueryRow(ctx,

registry/postgres_live_test.go

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,6 @@ func TestPostgresStores_Live(t *testing.T) {
164164
return registry.BlobEncryptionParams{
165165
Space: space,
166166
Digest: d,
167-
RegionWrappedCEK: []byte{0x00, 0xde, 0xad, 0xbe, 0xef},
168-
RegionKeyVersion: "region-v1",
169167
TenantRecipientKID: "did:key:tenant#wrap",
170168
HeaderLen: 212,
171169
BaseNonce: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
@@ -190,56 +188,7 @@ func TestPostgresStores_Live(t *testing.T) {
190188
}
191189
})
192190

193-
t.Run("encryption params re-wrap in place", func(t *testing.T) {
194-
space := testutil.RandomDID(t)
195-
encDigest := []byte{0x03, 0x04}
196-
if err := r.PutEncryptionParams(ctx, liveFEEParams(space, encDigest)); err != nil {
197-
t.Fatalf("PutEncryptionParams: %v", err)
198-
}
199-
want := liveFEEParams(space, encDigest)
200-
want.RegionWrappedCEK = []byte{0x11, 0x22, 0x33}
201-
want.RegionKeyVersion = "region-v2"
202-
if err := r.RewrapEncryptionParams(ctx, space, encDigest, want.RegionWrappedCEK, want.RegionKeyVersion); err != nil {
203-
t.Fatalf("RewrapEncryptionParams: %v", err)
204-
}
205-
got, err := r.GetEncryptionParams(ctx, space, encDigest)
206-
if err != nil {
207-
t.Fatalf("GetEncryptionParams: %v", err)
208-
}
209-
if !reflect.DeepEqual(*got, want) {
210-
t.Fatalf("after re-wrap = %+v, want %+v", *got, want)
211-
}
212-
})
213-
214-
t.Run("re-wrap of an absent row is not found", func(t *testing.T) {
215-
// The UPDATE matches nothing, which RowsAffected turns into ErrNotFound.
216-
space := testutil.RandomDID(t)
217-
err := r.RewrapEncryptionParams(ctx, space, []byte{0x0a, 0x0b}, []byte{0x11}, "region-v2")
218-
if !errors.Is(err, registry.ErrNotFound) {
219-
t.Fatalf("RewrapEncryptionParams(absent) = %v, want ErrNotFound", err)
220-
}
221-
})
222-
223-
t.Run("incomplete re-wrap rejected", func(t *testing.T) {
224-
space := testutil.RandomDID(t)
225-
encDigest := []byte{0x0c, 0x0d}
226-
want := liveFEEParams(space, encDigest)
227-
if err := r.PutEncryptionParams(ctx, want); err != nil {
228-
t.Fatalf("PutEncryptionParams: %v", err)
229-
}
230-
if err := r.RewrapEncryptionParams(ctx, space, encDigest, nil, "region-v2"); !errors.Is(err, registry.ErrInvalidEncryptionParams) {
231-
t.Fatalf("RewrapEncryptionParams(no CEK) = %v, want ErrInvalidEncryptionParams", err)
232-
}
233-
got, err := r.GetEncryptionParams(ctx, space, encDigest)
234-
if err != nil {
235-
t.Fatalf("GetEncryptionParams: %v", err)
236-
}
237-
if !reflect.DeepEqual(*got, want) {
238-
t.Fatalf("rejected re-wrap altered the row: %+v", *got)
239-
}
240-
})
241-
242-
t.Run("encryption params delete shreds", func(t *testing.T) {
191+
t.Run("encryption params delete removes the row", func(t *testing.T) {
243192
space := testutil.RandomDID(t)
244193
encDigest := []byte{0x05, 0x06}
245194
if err := r.PutEncryptionParams(ctx, liveFEEParams(space, encDigest)); err != nil {

0 commit comments

Comments
 (0)