From f197e5a4d14e8565313242241bf679436c99438a Mon Sep 17 00:00:00 2001 From: John Mulligan Date: Wed, 22 Jan 2025 16:37:26 -0500 Subject: [PATCH] rbd: add EncryptionLoad2 implementing rbd_encryption_load2 Add a new Image method EncryptionLoad2 implementing rbd_encryption_load2. This method adds the ability to have different encryption schemes across parent images. Signed-off-by: John Mulligan Fixes: #1059 --- docs/api-status.json | 6 + docs/api-status.md | 1 + rbd/encryption_load2.go | 63 +++++++++ rbd/encryption_load2_test.go | 261 +++++++++++++++++++++++++++++++++++ 4 files changed, 331 insertions(+) create mode 100644 rbd/encryption_load2.go create mode 100644 rbd/encryption_load2_test.go diff --git a/docs/api-status.json b/docs/api-status.json index 6e0803296..6b8e24177 100644 --- a/docs/api-status.json +++ b/docs/api-status.json @@ -1953,6 +1953,12 @@ "comment": "GroupSnapGetInfo returns a slice of RBD image snapshots that are part of a\ngroup snapshot.\n\nImplements:\n\n\tint rbd_group_snap_get_info(rados_ioctx_t group_p,\n\t const char *group_name,\n\t const char *snap_name,\n\t rbd_group_snap_info2_t *snaps);\n", "added_in_version": "v0.30.0", "expected_stable_version": "v0.32.0" + }, + { + "name": "Image.EncryptionLoad2", + "comment": "EncryptionLoad2 enables IO on an open encrypted image and ancestor images.\nThe first EncryptionOptions in the slice is applied to the image, the second\nto the first ancestor, the third to the second ancestor and so on.\nIf the length of the slice is smaller than the number of ancestors the\nfinal item in the slice will be applied to all remaining ancestors.\n\nImplements:\n\n\tint rbd_encryption_load2(rbd_image_t image,\n\t const rbd_encryption_spec_t *specs,\n\t size_t spec_count);\n", + "added_in_version": "$NEXT_RELEASE", + "expected_stable_version": "$NEXT_RELEASE_STABLE" } ] }, diff --git a/docs/api-status.md b/docs/api-status.md index e2865fadb..4f301e43b 100644 --- a/docs/api-status.md +++ b/docs/api-status.md @@ -25,6 +25,7 @@ Conn.GetAddrs | v0.31.0 | v0.33.0 | Name | Added in Version | Expected Stable Version | ---- | ---------------- | ----------------------- | GroupSnapGetInfo | v0.30.0 | v0.32.0 | +Image.EncryptionLoad2 | $NEXT_RELEASE | $NEXT_RELEASE_STABLE | ### Deprecated APIs diff --git a/rbd/encryption_load2.go b/rbd/encryption_load2.go new file mode 100644 index 000000000..5bcbd7dbb --- /dev/null +++ b/rbd/encryption_load2.go @@ -0,0 +1,63 @@ +//go:build !octopus && !pacific && !quincy && ceph_preview + +package rbd + +// #cgo LDFLAGS: -lrbd +// /* force XSI-complaint strerror_r() */ +// #define _POSIX_C_SOURCE 200112L +// #undef _GNU_SOURCE +// #include +import "C" + +import ( + "unsafe" +) + +// toEncryptionSpec returns a rbd_encryption_spec_t converted from the +// cEncryptionData type. +func (edata cEncryptionData) toEncryptionSpec() C.rbd_encryption_spec_t { + var cSpec C.rbd_encryption_spec_t + cSpec.format = edata.format + cSpec.opts = edata.opts + cSpec.opts_size = edata.optsSize + return cSpec +} + +// EncryptionLoad2 enables IO on an open encrypted image and ancestor images. +// The first EncryptionOptions in the slice is applied to the image, the second +// to the first ancestor, the third to the second ancestor and so on. +// If the length of the slice is smaller than the number of ancestors the +// final item in the slice will be applied to all remaining ancestors. +// +// Implements: +// +// int rbd_encryption_load2(rbd_image_t image, +// const rbd_encryption_spec_t *specs, +// size_t spec_count); +func (image *Image) EncryptionLoad2(opts []EncryptionOptions) error { + if image.image == nil { + return ErrImageNotOpen + } + + length := len(opts) + eos := make([]cEncryptionData, length) + cspecs := (*C.rbd_encryption_spec_t)(C.malloc( + C.size_t(C.sizeof_rbd_encryption_spec_t * length))) + specs := unsafe.Slice(cspecs, length) + + for idx, option := range opts { + eos[idx] = option.allocateEncryptionOptions() + specs[idx] = eos[idx].toEncryptionSpec() + } + defer func() { + for _, eopt := range eos { + eopt.free() + } + }() + + ret := C.rbd_encryption_load2( + image.image, + cspecs, + C.size_t(length)) + return getError(ret) +} diff --git a/rbd/encryption_load2_test.go b/rbd/encryption_load2_test.go new file mode 100644 index 000000000..8c834e74a --- /dev/null +++ b/rbd/encryption_load2_test.go @@ -0,0 +1,261 @@ +//go:build !octopus && !pacific && !quincy && ceph_preview + +package rbd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncryptionLoad2(t *testing.T) { + conn := radosConnect(t) + defer conn.Shutdown() + + poolname := GetUUID() + err := conn.MakePool(poolname) + assert.NoError(t, err) + defer conn.DeletePool(poolname) + + ioctx, err := conn.OpenIOContext(poolname) + require.NoError(t, err) + defer ioctx.Destroy() + + name := GetUUID() + testImageSize := uint64(50) * 1024 * 1024 + options := NewRbdImageOptions() + assert.NoError(t, + options.SetUint64(ImageOptionOrder, uint64(testImageOrder))) + err = CreateImage(ioctx, name, testImageSize, options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + + encOpts := EncryptionOptionsLUKS2{ + Alg: EncryptionAlgorithmAES256, + Passphrase: []byte("test-password"), + } + err = img.EncryptionFormat(encOpts) + assert.NoError(t, err) + + // close the image so we can reopen it and load the encryption info + // then write some encrypted data at the end of the image + err = img.Close() + assert.NoError(t, err) + defer func() { + assert.NoError(t, img.Remove()) + }() + + testData := []byte("Jinxed wizards pluck ivy from the big quilt") + var offset int64 + + t.Run("prepare", func(t *testing.T) { + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad2([]EncryptionOptions{encOpts}) + assert.NoError(t, err) + + stats, err := img.Stat() + require.NoError(t, err) + offset = int64(stats.Size) - int64(len(testData)) + + nOut, err := img.WriteAt(testData, offset) + assert.Equal(t, len(testData), nOut) + assert.NoError(t, err) + }) + + t.Run("readEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image, load the encryption format, and read the encrypted data + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionLoad2([]EncryptionOptions{encOpts}) + assert.NoError(t, err) + + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(inData)) + assert.Equal(t, inData, testData) + assert.NoError(t, err) + }) + + t.Run("noEnc", func(t *testing.T) { + require.NotEqual(t, offset, 0) + // Re-open the image and attempt to read the encrypted data without loading the encryption + img, err = OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + inData := make([]byte, len(testData)) + nIn, err := img.ReadAt(inData, offset) + assert.Equal(t, nIn, len(inData)) + assert.NotEqual(t, inData, testData) + assert.NoError(t, err) + }) +} + +func TestEncryptionLoad2WithParents(t *testing.T) { + dlength := int64(32) + testData1 := []byte("Very nice object ahead of change") + testData2 := []byte("A nice object encryption applied") + testData3 := []byte("A good object encryption abounds") + testData4 := []byte("Another portion is here and well") + written := [][]byte{} + assert.EqualValues(t, len(testData1), dlength) + assert.EqualValues(t, len(testData2), dlength) + assert.EqualValues(t, len(testData3), dlength) + assert.EqualValues(t, len(testData4), dlength) + + encOpts1 := EncryptionOptionsLUKS1{ + Alg: EncryptionAlgorithmAES128, + Passphrase: []byte("test-password"), + } + encOpts2 := EncryptionOptionsLUKS2{ + Alg: EncryptionAlgorithmAES128, + Passphrase: []byte("test-password"), + } + encOpts3 := EncryptionOptionsLUKS2{ + Alg: EncryptionAlgorithmAES256, + Passphrase: []byte("something-stronger"), + } + + conn := radosConnect(t) + defer conn.Shutdown() + + poolname := GetUUID() + err := conn.MakePool(poolname) + assert.NoError(t, err) + defer conn.DeletePool(poolname) + + ioctx, err := conn.OpenIOContext(poolname) + require.NoError(t, err) + defer ioctx.Destroy() + + name := GetUUID() + testImageSize := uint64(256) * 1024 * 1024 + options := NewRbdImageOptions() + assert.NoError(t, + options.SetUint64(ImageOptionOrder, uint64(testImageOrder))) + err = CreateImage(ioctx, name, testImageSize, options) + assert.NoError(t, err) + + t.Run("prepare", func(t *testing.T) { + img, err := OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + _, err = img.WriteAt(testData1, 0) + assert.NoError(t, err) + written = append(written, testData1) + }) + + t.Run("createClone1", func(t *testing.T) { + require.Len(t, written, 1) + parent, err := OpenImage(ioctx, name, NoSnapshot) + assert.NoError(t, err) + defer parent.Close() + snap, err := parent.CreateSnapshot("sn1") + assert.NoError(t, err) + err = snap.Protect() + assert.NoError(t, err) + + err = CloneImage(ioctx, name, "sn1", ioctx, name+"clone1", options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, name+"clone1", NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionFormat(encOpts1) + assert.NoError(t, err) + + err = img.EncryptionLoad2([]EncryptionOptions{encOpts1}) + assert.NoError(t, err) + _, err = img.WriteAt(testData2, dlength) + assert.NoError(t, err) + written = append(written, testData2) + }) + + t.Run("createClone2", func(t *testing.T) { + require.Len(t, written, 2) + parentName := name + "clone1" + cloneName := name + "clone2" + + parent, err := OpenImage(ioctx, parentName, NoSnapshot) + assert.NoError(t, err) + defer parent.Close() + snap, err := parent.CreateSnapshot("sn2") + assert.NoError(t, err) + err = snap.Protect() + assert.NoError(t, err) + + err = CloneImage(ioctx, parentName, "sn2", ioctx, cloneName, options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, cloneName, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionFormat(encOpts2) + assert.NoError(t, err) + + err = img.EncryptionLoad2([]EncryptionOptions{encOpts2, encOpts1}) + assert.NoError(t, err) + _, err = img.WriteAt(testData3, dlength*2) + assert.NoError(t, err) + written = append(written, testData3) + }) + + t.Run("createClone3", func(t *testing.T) { + require.Len(t, written, 3) + parentName := name + "clone2" + cloneName := name + "clone3" + + parent, err := OpenImage(ioctx, parentName, NoSnapshot) + assert.NoError(t, err) + defer parent.Close() + snap, err := parent.CreateSnapshot("sn3") + assert.NoError(t, err) + err = snap.Protect() + assert.NoError(t, err) + + err = CloneImage(ioctx, parentName, "sn3", ioctx, cloneName, options) + assert.NoError(t, err) + + img, err := OpenImage(ioctx, cloneName, NoSnapshot) + assert.NoError(t, err) + defer img.Close() + err = img.EncryptionFormat(encOpts3) + assert.NoError(t, err) + + err = img.EncryptionLoad2([]EncryptionOptions{ + encOpts3, encOpts2, encOpts1, + }) + assert.NoError(t, err) + _, err = img.WriteAt(testData4, dlength*3) + assert.NoError(t, err) + written = append(written, testData4) + }) + + t.Run("readAll", func(t *testing.T) { + require.Len(t, written, 4) + img, err := OpenImage(ioctx, name+"clone3", NoSnapshot) + assert.NoError(t, err) + defer img.Close() + + err = img.EncryptionLoad2([]EncryptionOptions{ + encOpts3, encOpts2, encOpts1, + }) + assert.NoError(t, err) + + inData := make([]byte, int(dlength)) + for idx, td := range written { + n, err := img.ReadAt(inData, int64(idx)*dlength) + assert.NoError(t, err) + assert.Len(t, inData, n) + assert.Equal(t, inData, td) + } + }) +}