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:
Otto Bittner 2023-10-02 09:00:38 +02:00
parent a7ceda37ea
commit 887dcda78b
15 changed files with 414 additions and 71 deletions

View File

@ -3050,6 +3050,7 @@ def go_dependencies():
sum = "h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as=", sum = "h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as=",
version = "v1.9.2", version = "v1.9.2",
) )
go_repository( go_repository(
name = "com_github_hexops_gotextdiff", name = "com_github_hexops_gotextdiff",
build_file_generation = "on", build_file_generation = "on",
@ -5017,6 +5018,15 @@ def go_dependencies():
sum = "h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=", sum = "h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=",
version = "v1.2.0", version = "v1.2.0",
) )
go_repository(
name = "com_github_tink_crypto_tink_go_v2",
build_file_generation = "on",
build_file_proto_mode = "disable_global",
importpath = "github.com/tink-crypto/tink-go/v2",
replace = "github.com/derpsteb/tink-go/v2",
sum = "h1:FVii9oXvddz9sFir5TRYjQKrzJLbVD/hibT+SnRSDzg=",
version = "v2.0.0-20231002051717-a808e454eed6",
)
go_repository( go_repository(
name = "com_github_titanous_rocacheck", name = "com_github_titanous_rocacheck",

2
go.mod
View File

@ -35,6 +35,7 @@ replace (
github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api => ./operators/constellation-node-operator/api github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api => ./operators/constellation-node-operator/api
github.com/google/go-tpm => github.com/thomasten/go-tpm v0.0.0-20230629092004-f43f8e2a59eb github.com/google/go-tpm => github.com/thomasten/go-tpm v0.0.0-20230629092004-f43f8e2a59eb
github.com/martinjungblut/go-cryptsetup => github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c github.com/martinjungblut/go-cryptsetup => github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c
github.com/tink-crypto/tink-go/v2 v2.0.0 => github.com/derpsteb/tink-go/v2 v2.0.0-20231002051717-a808e454eed6
) )
require ( require (
@ -108,6 +109,7 @@ require (
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.8.4
github.com/theupdateframework/go-tuf v0.5.2 github.com/theupdateframework/go-tuf v0.5.2
github.com/tink-crypto/tink-go/v2 v2.0.0
go.uber.org/goleak v1.2.1 go.uber.org/goleak v1.2.1
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.org/x/crypto v0.13.0 golang.org/x/crypto v0.13.0

2
go.sum
View File

@ -298,6 +298,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/derpsteb/tink-go/v2 v2.0.0-20231002051717-a808e454eed6 h1:FVii9oXvddz9sFir5TRYjQKrzJLbVD/hibT+SnRSDzg=
github.com/derpsteb/tink-go/v2 v2.0.0-20231002051717-a808e454eed6/go.mod h1:QAbyq9LZncomYnScxlfaHImbV4ieNIe6bnu/Xcqqox4=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=

View File

@ -57,7 +57,10 @@ func main() {
func runServer(flags cmdFlags, log *logger.Logger) error { func runServer(flags cmdFlags, log *logger.Logger) error {
log.With(zap.String("ip", flags.ip), zap.Int("port", defaultPort), zap.String("region", flags.region)).Infof("listening") log.With(zap.String("ip", flags.ip), zap.Int("port", defaultPort), zap.String("region", flags.region)).Infof("listening")
router := router.New(flags.region, log) router, err := router.New(flags.region, flags.kmsEndpoint, log)
if err != nil {
return fmt.Errorf("creating router: %w", err)
}
server := http.Server{ server := http.Server{
Addr: fmt.Sprintf("%s:%d", flags.ip, defaultPort), Addr: fmt.Sprintf("%s:%d", flags.ip, defaultPort),
@ -92,6 +95,7 @@ func parseFlags() (cmdFlags, error) {
ip := flag.String("ip", defaultIP, "ip to listen on") ip := flag.String("ip", defaultIP, "ip to listen on")
region := flag.String("region", defaultRegion, "AWS region in which target bucket is located") region := flag.String("region", defaultRegion, "AWS region in which target bucket is located")
certLocation := flag.String("cert", defaultCertLocation, "location of TLS certificate") certLocation := flag.String("cert", defaultCertLocation, "location of TLS certificate")
kmsEndpoint := flag.String("kms", "key-service.kube-system:9000", "endpoint of the KMS service to get key encryption keys from")
level := flag.Int("level", defaultLogLevel, "log level") level := flag.Int("level", defaultLogLevel, "log level")
flag.Parse() flag.Parse()
@ -107,7 +111,14 @@ func parseFlags() (cmdFlags, error) {
// return cmdFlags{}, fmt.Errorf("parsing log level: %w", err) // return cmdFlags{}, fmt.Errorf("parsing log level: %w", err)
// } // }
return cmdFlags{noTLS: *noTLS, ip: netIP.String(), region: *region, certLocation: *certLocation, logLevel: *level}, nil return cmdFlags{
noTLS: *noTLS,
ip: netIP.String(),
region: *region,
certLocation: *certLocation,
kmsEndpoint: *kmsEndpoint,
logLevel: *level,
}, nil
} }
type cmdFlags struct { type cmdFlags struct {
@ -115,6 +126,7 @@ type cmdFlags struct {
ip string ip string
region string region string
certLocation string certLocation string
kmsEndpoint string
// TODO(derpsteb): enable once we are on go 1.21. // TODO(derpsteb): enable once we are on go 1.21.
// logLevel slog.Level // logLevel slog.Level
logLevel int logLevel int

View File

@ -1,5 +1,8 @@
# Deploying s3proxy # Deploying s3proxy
**Caution:** Using s3proxy outside Constellation is insecure as the connection between the key management service (KMS) and s3proxy is protected by Constellation's WireGuard VPN.
The VPN is a feature of Constellation and will not be present by default in other environments.
Disclaimer: the following steps will be automated next. Disclaimer: the following steps will be automated next.
- Within `constellation/build`: `bazel run //:devbuild` - Within `constellation/build`: `bazel run //:devbuild`
- Copy the container name displayed for the s3proxy image. Look for the line starting with `[@//bazel/release:s3proxy_push]`. - Copy the container name displayed for the s3proxy image. Look for the line starting with `[@//bazel/release:s3proxy_push]`.

View File

@ -1,8 +1,24 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "crypto", name = "crypto",
srcs = ["crypto.go"], srcs = ["crypto.go"],
importpath = "github.com/edgelesssys/constellation/v2/s3proxy/internal/crypto", importpath = "github.com/edgelesssys/constellation/v2/s3proxy/internal/crypto",
visibility = ["//s3proxy:__subpackages__"], visibility = ["//s3proxy:__subpackages__"],
deps = [
"@com_github_tink_crypto_tink_go_v2//aead/subtle",
"@com_github_tink_crypto_tink_go_v2//kwp/subtle",
"@com_github_tink_crypto_tink_go_v2//subtle/random",
],
)
go_test(
name = "crypto_test",
srcs = ["crypto_test.go"],
embed = [":crypto"],
deps = [
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
) )

View File

@ -7,73 +7,67 @@ SPDX-License-Identifier: AGPL-3.0-only
/* /*
Package crypto provides encryption and decryption functions for the s3proxy. Package crypto provides encryption and decryption functions for the s3proxy.
It uses AES-256-GCM to encrypt and decrypt data. It uses AES-256-GCM to encrypt and decrypt data.
A new nonce is generated for each encryption operation.
*/ */
package crypto package crypto
import ( import (
"crypto/aes" "fmt"
"crypto/cipher"
"crypto/rand" aeadsubtle "github.com/tink-crypto/tink-go/v2/aead/subtle"
"io" kwpsubtle "github.com/tink-crypto/tink-go/v2/kwp/subtle"
"github.com/tink-crypto/tink-go/v2/subtle/random"
) )
// Encrypt takes a 32 byte key and encrypts a plaintext using AES-256-GCM. // Encrypt generates a random key to encrypt a plaintext using AES-256-GCM.
// Output format is 12 byte nonce + ciphertext. // The generated key is encrypted using the supplied key encryption key (KEK).
func Encrypt(plaintext, key []byte) ([]byte, error) { // The ciphertext and encrypted data encryption key (DEK) are returned.
// Enforce AES-256 func Encrypt(plaintext []byte, kek [32]byte) (ciphertext []byte, encryptedDEK []byte, err error) {
if len(key) != 32 { dek := random.GetRandomBytes(32)
return nil, aes.KeySizeError(len(key)) aesgcm, err := aeadsubtle.NewAESGCMSIV(dek)
}
// None should not be reused more often that 2^32 times:
// https://pkg.go.dev/crypto/cipher#NewGCM
// Assuming n encryption operations per second, the key has to be rotated every:
// n=1: 2^32 / (60*60*24*365*10) = 135 years.
// n=10: 2^32 / (60*60*24*365*10) = 13.5 years.
// n=100: 2^32 / (60*60*24*365*10) = 1.3 years.
// n=1000: 2^32 / (60*60*24*365*10) = 50 days.
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
block, err := aes.NewCipher(key)
if err != nil { if err != nil {
return nil, err return nil, nil, fmt.Errorf("getting aesgcm: %w", err)
} }
aesgcm, err := cipher.NewGCM(block)
ciphertext, err = aesgcm.Encrypt(plaintext, []byte(""))
if err != nil { if err != nil {
return nil, err return nil, nil, fmt.Errorf("encrypting plaintext: %w", err)
} }
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
// Prepend the nonce to the ciphertext. keywrapper, err := kwpsubtle.NewKWP(kek[:])
ciphertext = append(nonce, ciphertext...) if err != nil {
return nil, nil, fmt.Errorf("getting kwp: %w", err)
}
return ciphertext, nil encryptedDEK, err = keywrapper.Wrap(dek)
if err != nil {
return nil, nil, fmt.Errorf("wrapping dek: %w", err)
}
return ciphertext, encryptedDEK, nil
} }
// Decrypt takes a 32 byte key and decrypts a ciphertext using AES-256-GCM. // Decrypt decrypts a ciphertext using AES-256-GCM.
// ciphertext is formatted as 12 byte nonce + ciphertext. // The encrypted DEK is decrypted using the supplied KEK.
func Decrypt(ciphertext []byte, key []byte) ([]byte, error) { func Decrypt(ciphertext, encryptedDEK []byte, kek [32]byte) ([]byte, error) {
// Enforce AES-256 keywrapper, err := kwpsubtle.NewKWP(kek[:])
if len(key) != 32 {
return nil, aes.KeySizeError(len(key))
}
// Extract the nonce from the ciphertext.
nonce := ciphertext[:12]
ciphertext = ciphertext[12:]
block, err := aes.NewCipher(key)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("getting kwp: %w", err)
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
} }
return aesgcm.Open(nil, nonce, ciphertext, nil) dek, err := keywrapper.Unwrap(encryptedDEK)
if err != nil {
return nil, fmt.Errorf("unwrapping dek: %w", err)
}
aesgcm, err := aeadsubtle.NewAESGCMSIV(dek)
if err != nil {
return nil, fmt.Errorf("getting aesgcm: %w", err)
}
plaintext, err := aesgcm.Decrypt(ciphertext, []byte(""))
if err != nil {
return nil, fmt.Errorf("decrypting ciphertext: %w", err)
}
return plaintext, nil
} }

View File

@ -0,0 +1,48 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package crypto
import (
"crypto/rand"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncryptDecrypt(t *testing.T) {
tests := map[string]struct {
plaintext []byte
}{
"simple": {
plaintext: []byte("hello, world"),
},
"long": {
plaintext: []byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor."),
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
kek := [32]byte{}
_, err := rand.Read(kek[:])
require.NoError(t, err)
ciphertext, encryptedDEK, err := Encrypt(tt.plaintext, kek)
require.NoError(t, err)
assert.NotContains(t, ciphertext, tt.plaintext)
// Decrypt the ciphertext using the KEK and encrypted DEK
decrypted, err := Decrypt(ciphertext, encryptedDEK, kek)
require.NoError(t, err)
// Verify that the decrypted plaintext matches the original plaintext
assert.Equal(t, tt.plaintext, decrypted, fmt.Sprintf("expected plaintext %s, got %s", tt.plaintext, decrypted))
})
}
}

View File

@ -0,0 +1,29 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "kms",
srcs = ["kms.go"],
importpath = "github.com/edgelesssys/constellation/v2/s3proxy/internal/kms",
visibility = ["//s3proxy:__subpackages__"],
deps = [
"//internal/logger",
"//keyservice/keyserviceproto",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//credentials/insecure",
],
)
go_test(
name = "kms_test",
srcs = ["kms_test.go"],
embed = [":kms"],
deps = [
"//internal/logger",
"//keyservice/keyserviceproto",
"@com_github_stretchr_testify//assert",
"@org_golang_google_grpc//:go_default_library",
"@org_golang_google_grpc//test/bufconn",
"@org_uber_go_goleak//:goleak",
],
)

View File

@ -0,0 +1,76 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
/*
Package kms is used to interact with the Constellation keyservice.
So far it is a copy of the joinservice's kms package.
*/
package kms
import (
"context"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/keyservice/keyserviceproto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// Client interacts with Constellation's keyservice.
type Client struct {
log *logger.Logger
endpoint string
grpc grpcClient
}
// New creates a new KMS.
func New(log *logger.Logger, endpoint string) Client {
return Client{
log: log,
endpoint: endpoint,
grpc: client{},
}
}
// GetDataKey returns a data encryption key for the given UUID.
func (c Client) GetDataKey(ctx context.Context, keyID string, length int) ([]byte, error) {
log := c.log.With("keyID", keyID, "endpoint", c.endpoint)
// the KMS does not use aTLS since traffic is only routed through the Constellation cluster
// cluster internal connections are considered trustworthy
log.Infof("Connecting to KMS")
conn, err := grpc.DialContext(ctx, c.endpoint, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, err
}
defer conn.Close()
log.Infof("Requesting data key")
res, err := c.grpc.GetDataKey(
ctx,
&keyserviceproto.GetDataKeyRequest{
DataKeyId: keyID,
Length: uint32(length),
},
conn,
)
if err != nil {
return nil, fmt.Errorf("fetching data encryption key from Constellation KMS: %w", err)
}
log.Infof("Data key request successful")
return res.DataKey, nil
}
type grpcClient interface {
GetDataKey(context.Context, *keyserviceproto.GetDataKeyRequest, *grpc.ClientConn) (*keyserviceproto.GetDataKeyResponse, error)
}
type client struct{}
func (c client) GetDataKey(ctx context.Context, req *keyserviceproto.GetDataKeyRequest, conn *grpc.ClientConn) (*keyserviceproto.GetDataKeyResponse, error) {
return keyserviceproto.NewAPIClient(conn).GetDataKey(ctx, req)
}

View File

@ -0,0 +1,72 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package kms
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/keyservice/keyserviceproto"
"github.com/stretchr/testify/assert"
"go.uber.org/goleak"
"google.golang.org/grpc"
"google.golang.org/grpc/test/bufconn"
)
type stubClient struct {
getDataKeyErr error
dataKey []byte
}
func (c *stubClient) GetDataKey(context.Context, *keyserviceproto.GetDataKeyRequest, *grpc.ClientConn) (*keyserviceproto.GetDataKeyResponse, error) {
return &keyserviceproto.GetDataKeyResponse{DataKey: c.dataKey}, c.getDataKeyErr
}
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
func TestGetDataKey(t *testing.T) {
testCases := map[string]struct {
client *stubClient
wantErr bool
}{
"GetDataKey success": {
client: &stubClient{dataKey: []byte{0x1, 0x2, 0x3}},
},
"GetDataKey error": {
client: &stubClient{getDataKeyErr: errors.New("error")},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
listener := bufconn.Listen(1)
defer listener.Close()
client := New(
logger.NewTest(t),
listener.Addr().String(),
)
client.grpc = tc.client
res, err := client.GetDataKey(context.Background(), "disk-uuid", 32)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.client.dataKey, res)
}
})
}
}

View File

@ -12,6 +12,7 @@ go_library(
deps = [ deps = [
"//internal/logger", "//internal/logger",
"//s3proxy/internal/crypto", "//s3proxy/internal/crypto",
"//s3proxy/internal/kms",
"//s3proxy/internal/s3", "//s3proxy/internal/s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3", "@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@org_uber_go_zap//:zap", "@org_uber_go_zap//:zap",

View File

@ -8,6 +8,7 @@ package router
import ( import (
"context" "context"
"encoding/hex"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@ -23,15 +24,14 @@ import (
) )
const ( const (
// testingKey is a temporary encryption key used for testing. // 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.
// TODO (derpsteb): This key needs to be fetched from Constellation's keyservice. // Use lowercase only, as AWS automatically lowercases all metadata keys.
testingKey = "01234567890123456789012345678901" dekTag = "constellation-dek"
// 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"
) )
// object bundles data to implement http.Handler methods that use data from incoming requests. // object bundles data to implement http.Handler methods that use data from incoming requests.
type object struct { type object struct {
kek [32]byte
client s3Client client s3Client
key string key string
bucket string bucket string
@ -113,10 +113,16 @@ func (o object) get(w http.ResponseWriter, r *http.Request) {
} }
plaintext := body 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, encryptedDEK, o.kek)
plaintext, err = crypto.Decrypt(body, []byte(testingKey))
if err != nil { if err != nil {
o.log.With(zap.Error(err)).Errorf("GetObject decrypting response") o.log.With(zap.Error(err)).Errorf("GetObject decrypting response")
http.Error(w, err.Error(), http.StatusInternalServerError) 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. // put is a http.HandlerFunc that implements the PUT method for objects.
func (o object) put(w http.ResponseWriter, r *http.Request) { func (o object) put(w http.ResponseWriter, r *http.Request) {
o.log.Debugf("putObject", "key", o.key, "host", o.bucket) ciphertext, encryptedDEK, err := crypto.Encrypt(o.data, o.kek)
ciphertext, err := crypto.Encrypt(o.data, []byte(testingKey))
if err != nil { if err != nil {
o.log.With(zap.Error(err)).Errorf("PutObject") o.log.With(zap.Error(err)).Errorf("PutObject")
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// We need to tag objects that are encrypted with this proxy, o.metadata[dekTag] = hex.EncodeToString(encryptedDEK)
// 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"
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) 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 { if err != nil {

View File

@ -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. 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. 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 package router
import ( import (
"bytes" "bytes"
"context"
"crypto/md5" "crypto/md5"
"crypto/sha256" "crypto/sha256"
"encoding/base64" "encoding/base64"
@ -28,10 +35,17 @@ import (
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/s3proxy/internal/kms"
"github.com/edgelesssys/constellation/v2/s3proxy/internal/s3" "github.com/edgelesssys/constellation/v2/s3proxy/internal/s3"
"go.uber.org/zap" "go.uber.org/zap"
) )
const (
// Use a 32*8 = 256 bit key for AES-256.
kekSizeBytes = 32
kekID = "s3proxy-kek"
)
var ( var (
keyPattern = regexp.MustCompile("/(.+)") keyPattern = regexp.MustCompile("/(.+)")
bucketAndKeyPattern = regexp.MustCompile("/([^/?]+)/(.+)") bucketAndKeyPattern = regexp.MustCompile("/([^/?]+)/(.+)")
@ -40,12 +54,26 @@ var (
// Router implements the interception logic for the s3proxy. // Router implements the interception logic for the s3proxy.
type Router struct { type Router struct {
region string region string
kek [32]byte
log *logger.Logger log *logger.Logger
} }
// New creates a new Router. // New creates a new Router.
func New(region string, log *logger.Logger) Router { func New(region, endpoint string, log *logger.Logger) (Router, error) {
return Router{region: region, log: log} 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. // 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. // 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. // In other cases the bucket name is sent as part of the path.
func containsBucket(host string) bool { func containsBucket(host string) bool {

View File

@ -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)
})
}
}