mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-02-09 03:18:34 -05: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
@ -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
2
go.mod
@ -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
2
go.sum
@ -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=
|
||||||
|
@ -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
|
||||||
|
@ -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]`.
|
||||||
|
@ -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",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
48
s3proxy/internal/crypto/crypto_test.go
Normal file
48
s3proxy/internal/crypto/crypto_test.go
Normal 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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
29
s3proxy/internal/kms/BUILD.bazel
Normal file
29
s3proxy/internal/kms/BUILD.bazel
Normal 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",
|
||||||
|
],
|
||||||
|
)
|
76
s3proxy/internal/kms/kms.go
Normal file
76
s3proxy/internal/kms/kms.go
Normal 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)
|
||||||
|
}
|
72
s3proxy/internal/kms/kms_test.go
Normal file
72
s3proxy/internal/kms/kms_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user