mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 14:26:23 -04:00
s3proxy: add keyservice integration
Encrypt each object with a random DEK and attach the encrypted DEK as object metadata. Encrpt the DEK with a key from the keyservice. All objects use the same KEK until a keyrotation takes place.
This commit is contained in:
parent
a7ceda37ea
commit
887dcda78b
15 changed files with 414 additions and 71 deletions
|
@ -12,6 +12,7 @@ go_library(
|
|||
deps = [
|
||||
"//internal/logger",
|
||||
"//s3proxy/internal/crypto",
|
||||
"//s3proxy/internal/kms",
|
||||
"//s3proxy/internal/s3",
|
||||
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
|
||||
"@org_uber_go_zap//:zap",
|
||||
|
|
|
@ -8,6 +8,7 @@ package router
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
@ -23,15 +24,14 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
// testingKey is a temporary encryption key used for testing.
|
||||
// TODO (derpsteb): This key needs to be fetched from Constellation's keyservice.
|
||||
testingKey = "01234567890123456789012345678901"
|
||||
// encryptionTag is the key used to tag objects that are encrypted with this proxy. Presence of the key implies the object needs to be decrypted.
|
||||
encryptionTag = "constellation-encryption"
|
||||
// dekTag is the name of the header that holds the encrypted data encryption key for the attached object. Presence of the key implies the object needs to be decrypted.
|
||||
// Use lowercase only, as AWS automatically lowercases all metadata keys.
|
||||
dekTag = "constellation-dek"
|
||||
)
|
||||
|
||||
// object bundles data to implement http.Handler methods that use data from incoming requests.
|
||||
type object struct {
|
||||
kek [32]byte
|
||||
client s3Client
|
||||
key string
|
||||
bucket string
|
||||
|
@ -113,10 +113,16 @@ func (o object) get(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
plaintext := body
|
||||
decrypt, ok := output.Metadata[encryptionTag]
|
||||
rawEncryptedDEK, ok := output.Metadata[dekTag]
|
||||
if ok {
|
||||
encryptedDEK, err := hex.DecodeString(rawEncryptedDEK)
|
||||
if err != nil {
|
||||
o.log.Errorf("GetObject decoding DEK", "error", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if ok && decrypt == "true" {
|
||||
plaintext, err = crypto.Decrypt(body, []byte(testingKey))
|
||||
plaintext, err = crypto.Decrypt(body, encryptedDEK, o.kek)
|
||||
if err != nil {
|
||||
o.log.With(zap.Error(err)).Errorf("GetObject decrypting response")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
|
@ -132,18 +138,13 @@ func (o object) get(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// put is a http.HandlerFunc that implements the PUT method for objects.
|
||||
func (o object) put(w http.ResponseWriter, r *http.Request) {
|
||||
o.log.Debugf("putObject", "key", o.key, "host", o.bucket)
|
||||
|
||||
ciphertext, err := crypto.Encrypt(o.data, []byte(testingKey))
|
||||
ciphertext, encryptedDEK, err := crypto.Encrypt(o.data, o.kek)
|
||||
if err != nil {
|
||||
o.log.With(zap.Error(err)).Errorf("PutObject")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// We need to tag objects that are encrypted with this proxy,
|
||||
// because there might be objects in a bucket that are not encrypted.
|
||||
// GetObject needs to be able to recognize these objects and skip decryption.
|
||||
o.metadata[encryptionTag] = "true"
|
||||
o.metadata[dekTag] = hex.EncodeToString(encryptedDEK)
|
||||
|
||||
output, err := o.client.PutObject(r.Context(), o.bucket, o.key, o.tags, o.contentType, o.objectLockLegalHoldStatus, o.objectLockMode, o.sseCustomerAlgorithm, o.sseCustomerKey, o.sseCustomerKeyMD5, o.objectLockRetainUntilDate, o.metadata, ciphertext)
|
||||
if err != nil {
|
||||
|
|
|
@ -10,11 +10,18 @@ It decides which packages to forward and which to intercept.
|
|||
|
||||
The routing logic in this file is taken from this blog post: https://benhoyt.com/writings/go-routing/#regex-switch.
|
||||
We should be able to replace this once this is part of the stdlib: https://github.com/golang/go/issues/61410.
|
||||
|
||||
If the router intercepts a PutObject request it will encrypt the body before forwarding it to the S3 API.
|
||||
The stored object will have a tag that holds an encrypted data encryption key (DEK).
|
||||
That DEK is used to encrypt the object's body.
|
||||
The DEK is generated randomly for each PutObject request.
|
||||
The DEK is encrypted with a key encryption key (KEK) fetched from Constellation's keyservice.
|
||||
*/
|
||||
package router
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
|
@ -28,10 +35,17 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/edgelesssys/constellation/v2/s3proxy/internal/kms"
|
||||
"github.com/edgelesssys/constellation/v2/s3proxy/internal/s3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
// Use a 32*8 = 256 bit key for AES-256.
|
||||
kekSizeBytes = 32
|
||||
kekID = "s3proxy-kek"
|
||||
)
|
||||
|
||||
var (
|
||||
keyPattern = regexp.MustCompile("/(.+)")
|
||||
bucketAndKeyPattern = regexp.MustCompile("/([^/?]+)/(.+)")
|
||||
|
@ -40,12 +54,26 @@ var (
|
|||
// Router implements the interception logic for the s3proxy.
|
||||
type Router struct {
|
||||
region string
|
||||
kek [32]byte
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
// New creates a new Router.
|
||||
func New(region string, log *logger.Logger) Router {
|
||||
return Router{region: region, log: log}
|
||||
func New(region, endpoint string, log *logger.Logger) (Router, error) {
|
||||
kms := kms.New(log, endpoint)
|
||||
|
||||
// Get the key encryption key that encrypts all DEKs.
|
||||
kek, err := kms.GetDataKey(context.Background(), kekID, kekSizeBytes)
|
||||
if err != nil {
|
||||
return Router{}, fmt.Errorf("getting KEK: %w", err)
|
||||
}
|
||||
|
||||
kekArray, err := byteSliceToByteArray(kek)
|
||||
if err != nil {
|
||||
return Router{}, fmt.Errorf("converting KEK to byte array: %w", err)
|
||||
}
|
||||
|
||||
return Router{region: region, kek: kekArray, log: log}, nil
|
||||
}
|
||||
|
||||
// Serve implements the routing logic for the s3 proxy.
|
||||
|
@ -243,6 +271,16 @@ func handleForwards(log *logger.Logger) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// byteSliceToByteArray casts a byte slice to a byte array of length 32.
|
||||
// It does a length check to prevent the cast from panic'ing.
|
||||
func byteSliceToByteArray(input []byte) ([32]byte, error) {
|
||||
if len(input) != 32 {
|
||||
return [32]byte{}, fmt.Errorf("input length mismatch, got: %d", len(input))
|
||||
}
|
||||
|
||||
return ([32]byte)(input), nil
|
||||
}
|
||||
|
||||
// containsBucket is a helper to recognizes cases where the bucket name is sent as part of the host.
|
||||
// In other cases the bucket name is sent as part of the path.
|
||||
func containsBucket(host string) bool {
|
||||
|
|
|
@ -46,3 +46,42 @@ func TestValidateContentMD5(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestByteSliceToByteArray(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input []byte
|
||||
output [32]byte
|
||||
wantErr bool
|
||||
}{
|
||||
"empty input": {
|
||||
input: []byte{},
|
||||
output: [32]byte{},
|
||||
},
|
||||
"successful input": {
|
||||
input: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
||||
output: [32]byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41},
|
||||
},
|
||||
"input too short": {
|
||||
input: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
||||
output: [32]byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41},
|
||||
wantErr: true,
|
||||
},
|
||||
"input too long": {
|
||||
input: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
|
||||
output: [32]byte{0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result, err := byteSliceToByteArray(tc.input)
|
||||
if tc.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.output, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue