Skip to content

Commit

Permalink
s3proxy: add keyservice integration
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
derpsteb committed Oct 6, 2023
1 parent a7ceda3 commit 887dcda
Show file tree
Hide file tree
Showing 15 changed files with 406 additions and 63 deletions.
10 changes: 10 additions & 0 deletions bazel/toolchains/go_module_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3050,6 +3050,7 @@ def go_dependencies():
sum = "h1:YjkZLJ7K3inKgMZ0wzCU9OHqc+UqMQyXsPXnf3Cl2as=",
version = "v1.9.2",
)

go_repository(
name = "com_github_hexops_gotextdiff",
build_file_generation = "on",
Expand Down Expand Up @@ -5017,6 +5018,15 @@ def go_dependencies():
sum = "h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=",
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(
name = "com_github_titanous_rocacheck",
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ replace (
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/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 (
Expand Down Expand Up @@ -108,6 +109,7 @@ require (
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.8.4
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/zap v1.26.0
golang.org/x/crypto v0.13.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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=
Expand Down
16 changes: 14 additions & 2 deletions s3proxy/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ func main() {
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")

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{
Addr: fmt.Sprintf("%s:%d", flags.ip, defaultPort),
Expand Down Expand Up @@ -92,6 +95,7 @@ func parseFlags() (cmdFlags, error) {
ip := flag.String("ip", defaultIP, "ip to listen on")
region := flag.String("region", defaultRegion, "AWS region in which target bucket is located")
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")

flag.Parse()
Expand All @@ -107,14 +111,22 @@ func parseFlags() (cmdFlags, error) {
// 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 {
noTLS bool
ip string
region string
certLocation string
kmsEndpoint string
// TODO(derpsteb): enable once we are on go 1.21.
// logLevel slog.Level
logLevel int
Expand Down
3 changes: 3 additions & 0 deletions s3proxy/deploy/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# 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.
- 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]`.
Expand Down
16 changes: 16 additions & 0 deletions s3proxy/internal/crypto/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")

go_library(
name = "crypto",
srcs = ["crypto.go"],
importpath = "github.com/edgelesssys/constellation/v2/s3proxy/internal/crypto",
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",
],
)
82 changes: 38 additions & 44 deletions s3proxy/internal/crypto/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,67 @@ SPDX-License-Identifier: AGPL-3.0-only
/*
Package crypto provides encryption and decryption functions for the s3proxy.
It uses AES-256-GCM to encrypt and decrypt data.
A new nonce is generated for each encryption operation.
*/
package crypto

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"io"
"fmt"

aeadsubtle "github.com/tink-crypto/tink-go/v2/aead/subtle"
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.
// Output format is 12 byte nonce + ciphertext.
func Encrypt(plaintext, key []byte) ([]byte, error) {
// Enforce AES-256
if len(key) != 32 {
return nil, aes.KeySizeError(len(key))
// Encrypt generates a random key to encrypt a plaintext using AES-256-GCM.
// The generated key is encrypted using the supplied key encryption key (KEK).
// The ciphertext and encrypted data encryption key (DEK) are returned.
func Encrypt(plaintext []byte, kek [32]byte) (ciphertext []byte, encryptedDEK []byte, err error) {
dek := random.GetRandomBytes(32)
aesgcm, err := aeadsubtle.NewAESGCMSIV(dek)
if err != nil {
return nil, nil, fmt.Errorf("getting aesgcm: %w", err)
}

// 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
ciphertext, err = aesgcm.Encrypt(plaintext, []byte(""))
if err != nil {
return nil, nil, fmt.Errorf("encrypting plaintext: %w", err)
}

block, err := aes.NewCipher(key)
keywrapper, err := kwpsubtle.NewKWP(kek[:])
if err != nil {
return nil, err
return nil, nil, fmt.Errorf("getting kwp: %w", err)
}
aesgcm, err := cipher.NewGCM(block)

encryptedDEK, err = keywrapper.Wrap(dek)
if err != nil {
return nil, err
return nil, nil, fmt.Errorf("wrapping dek: %w", err)
}
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)

// Prepend the nonce to the ciphertext.
ciphertext = append(nonce, ciphertext...)

return ciphertext, nil
return ciphertext, encryptedDEK, nil
}

// Decrypt takes a 32 byte key and decrypts a ciphertext using AES-256-GCM.
// ciphertext is formatted as 12 byte nonce + ciphertext.
func Decrypt(ciphertext []byte, key []byte) ([]byte, error) {
// Enforce AES-256
if len(key) != 32 {
return nil, aes.KeySizeError(len(key))
// Decrypt decrypts a ciphertext using AES-256-GCM.
// The encrypted DEK is decrypted using the supplied KEK.
func Decrypt(ciphertext, encryptedDEK []byte, kek [32]byte) ([]byte, error) {
keywrapper, err := kwpsubtle.NewKWP(kek[:])
if err != nil {
return nil, fmt.Errorf("getting kwp: %w", err)
}

// Extract the nonce from the ciphertext.
nonce := ciphertext[:12]
ciphertext = ciphertext[12:]
dek, err := keywrapper.Unwrap(encryptedDEK)
if err != nil {
return nil, fmt.Errorf("unwrapping dek: %w", err)
}

block, err := aes.NewCipher(key)
aesgcm, err := aeadsubtle.NewAESGCMSIV(dek)
if err != nil {
return nil, err
return nil, fmt.Errorf("getting aesgcm: %w", err)
}
aesgcm, err := cipher.NewGCM(block)

plaintext, err := aesgcm.Decrypt(ciphertext, []byte(""))
if err != nil {
return nil, err
return nil, fmt.Errorf("decrypting ciphertext: %w", err)
}

return aesgcm.Open(nil, nonce, ciphertext, nil)
return plaintext, nil
}
48 changes: 48 additions & 0 deletions s3proxy/internal/crypto/crypto_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
}
29 changes: 29 additions & 0 deletions s3proxy/internal/kms/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
76 changes: 76 additions & 0 deletions s3proxy/internal/kms/kms.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 887dcda

Please sign in to comment.