OP#792 initial commit after fork
This commit is contained in:
441
internal/kms/config.go
Normal file
441
internal/kms/config.go
Normal file
@@ -0,0 +1,441 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"aead.dev/mtls"
|
||||
"github.com/minio/kms-go/kes"
|
||||
"github.com/minio/kms-go/kms"
|
||||
"github.com/minio/pkg/v3/certs"
|
||||
"github.com/minio/pkg/v3/ellipses"
|
||||
"github.com/minio/pkg/v3/env"
|
||||
)
|
||||
|
||||
// Environment variables for MinIO KMS.
|
||||
const (
|
||||
EnvKMSEndpoint = "MINIO_KMS_SERVER" // List of MinIO KMS endpoints, separated by ','
|
||||
EnvKMSEnclave = "MINIO_KMS_ENCLAVE" // MinIO KMS enclave in which the key and identity exists
|
||||
EnvKMSDefaultKey = "MINIO_KMS_SSE_KEY" // Default key used for SSE-S3 or when no SSE-KMS key ID is specified
|
||||
EnvKMSAPIKey = "MINIO_KMS_API_KEY" // Credential to access the MinIO KMS.
|
||||
)
|
||||
|
||||
// Environment variables for MinIO KES.
|
||||
const (
|
||||
EnvKESEndpoint = "MINIO_KMS_KES_ENDPOINT" // One or multiple KES endpoints, separated by ','
|
||||
EnvKESDefaultKey = "MINIO_KMS_KES_KEY_NAME" // The default key name used for IAM data and when no key ID is specified on a bucket
|
||||
EnvKESAPIKey = "MINIO_KMS_KES_API_KEY" // Access credential for KES - API keys and private key / certificate are mutually exclusive
|
||||
EnvKESClientKey = "MINIO_KMS_KES_KEY_FILE" // Path to TLS private key for authenticating to KES with mTLS - usually prefer API keys
|
||||
EnvKESClientCert = "MINIO_KMS_KES_CERT_FILE" // Path to TLS certificate for authenticating to KES with mTLS - usually prefer API keys
|
||||
EnvKESServerCA = "MINIO_KMS_KES_CAPATH" // Path to file/directory containing CA certificates to verify the KES server certificate
|
||||
EnvKESClientPassword = "MINIO_KMS_KES_KEY_PASSWORD" // Optional password to decrypt an encrypt TLS private key
|
||||
)
|
||||
|
||||
// Environment variables for static KMS key.
|
||||
const (
|
||||
EnvKMSSecretKey = "MINIO_KMS_SECRET_KEY" // Static KMS key in the form "<key-name>:<base64-32byte-key>". Implements a subset of KMS/KES APIs
|
||||
EnvKMSSecretKeyFile = "MINIO_KMS_SECRET_KEY_FILE" // Path to a file to read the static KMS key from
|
||||
)
|
||||
|
||||
// EnvKMSReplicateKeyID is an env. variable that controls whether MinIO
|
||||
// replicates the KMS key ID. By default, KMS key ID replication is enabled
|
||||
// but can be turned off.
|
||||
const EnvKMSReplicateKeyID = "MINIO_KMS_REPLICATE_KEYID"
|
||||
|
||||
const (
|
||||
tlsClientSessionCacheSize = 100
|
||||
)
|
||||
|
||||
var replicateKeyID = sync.OnceValue(func() bool {
|
||||
if v, ok := os.LookupEnv(EnvKMSReplicateKeyID); ok && strings.ToLower(v) == "off" {
|
||||
return false
|
||||
}
|
||||
return true // by default, replicating KMS key IDs is enabled
|
||||
})
|
||||
|
||||
// ReplicateKeyID reports whether KMS key IDs should be included when
|
||||
// replicating objects. It's enabled by default. To disable it, set:
|
||||
//
|
||||
// MINIO_KMS_REPLICATE_KEYID=off
|
||||
//
|
||||
// Some deployments use different KMS clusters with destinct keys on
|
||||
// each site. Trying to replicate the KMS key ID can cause requests
|
||||
// to fail in such setups.
|
||||
func ReplicateKeyID() bool { return replicateKeyID() }
|
||||
|
||||
// ConnectionOptions is a structure containing options for connecting
|
||||
// to a KMS.
|
||||
type ConnectionOptions struct {
|
||||
CADir string // Path to directory (or file) containing CA certificates
|
||||
}
|
||||
|
||||
// Connect returns a new Conn to a KMS. It uses configuration from the
|
||||
// environment and returns a:
|
||||
//
|
||||
// - connection to MinIO KMS if the "MINIO_KMS_SERVER" variable is present.
|
||||
// - connection to MinIO KES if the "MINIO_KMS_KES_ENDPOINT" is present.
|
||||
// - connection to a "local" KMS implementation using a static key if the
|
||||
// "MINIO_KMS_SECRET_KEY" or "MINIO_KMS_SECRET_KEY_FILE" is present.
|
||||
//
|
||||
// It returns an error if connecting to the KMS implementation fails,
|
||||
// e.g. due to incomplete config, or when configurations for multiple
|
||||
// KMS implementations are present.
|
||||
func Connect(ctx context.Context, opts *ConnectionOptions) (*KMS, error) {
|
||||
if present, err := IsPresent(); !present || err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New("kms: no KMS configuration specified")
|
||||
}
|
||||
|
||||
lookup := func(key string) bool {
|
||||
_, ok := os.LookupEnv(key)
|
||||
return ok
|
||||
}
|
||||
switch {
|
||||
case lookup(EnvKMSEndpoint):
|
||||
rawEndpoint := env.Get(EnvKMSEndpoint, "")
|
||||
if rawEndpoint == "" {
|
||||
return nil, errors.New("kms: no KMS server endpoint provided")
|
||||
}
|
||||
endpoints, err := expandEndpoints(rawEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := mtls.ParsePrivateKey(env.Get(EnvKMSAPIKey, ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rootCAs *x509.CertPool
|
||||
if opts != nil && opts.CADir != "" {
|
||||
rootCAs, err = certs.GetRootCAs(opts.CADir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client, err := kms.NewClient(&kms.Config{
|
||||
Endpoints: endpoints,
|
||||
APIKey: key,
|
||||
TLS: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
|
||||
RootCAs: rootCAs,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &KMS{
|
||||
Type: MinKMS,
|
||||
DefaultKey: env.Get(EnvKMSDefaultKey, ""),
|
||||
conn: &kmsConn{
|
||||
enclave: env.Get(EnvKMSEnclave, ""),
|
||||
defaultKey: env.Get(EnvKMSDefaultKey, ""),
|
||||
client: client,
|
||||
},
|
||||
latencyBuckets: defaultLatencyBuckets,
|
||||
latency: make([]atomic.Uint64, len(defaultLatencyBuckets)),
|
||||
}, nil
|
||||
case lookup(EnvKESEndpoint):
|
||||
rawEndpoint := env.Get(EnvKESEndpoint, "")
|
||||
if rawEndpoint == "" {
|
||||
return nil, errors.New("kms: no KES server endpoint provided")
|
||||
}
|
||||
endpoints, err := expandEndpoints(rawEndpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
|
||||
}
|
||||
if s := env.Get(EnvKESAPIKey, ""); s != "" {
|
||||
key, err := kes.ParseAPIKey(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := kes.GenerateCertificate(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { return &cert, nil }
|
||||
} else {
|
||||
loadX509KeyPair := func(certFile, keyFile string) (tls.Certificate, error) {
|
||||
// Manually load the certificate and private key into memory.
|
||||
// We need to check whether the private key is encrypted, and
|
||||
// if so, decrypt it using the user-provided password.
|
||||
certBytes, err := os.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err)
|
||||
}
|
||||
keyBytes, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("Unable to load KES client private key as specified by the shell environment: %v", err)
|
||||
}
|
||||
privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes))
|
||||
if len(rest) != 0 {
|
||||
return tls.Certificate{}, errors.New("Unable to load KES client private key as specified by the shell environment: private key contains additional data")
|
||||
}
|
||||
if x509.IsEncryptedPEMBlock(privateKeyPEM) {
|
||||
keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(EnvKESClientPassword, "")))
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("Unable to decrypt KES client private key as specified by the shell environment: %v", err)
|
||||
}
|
||||
keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes})
|
||||
}
|
||||
certificate, err := tls.X509KeyPair(certBytes, keyBytes)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err)
|
||||
}
|
||||
return certificate, nil
|
||||
}
|
||||
|
||||
certificate, err := certs.NewCertificate(env.Get(EnvKESClientCert, ""), env.Get(EnvKESClientKey, ""), loadX509KeyPair)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificate.Watch(ctx, 15*time.Minute, syscall.SIGHUP)
|
||||
|
||||
conf.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||
cert := certificate.Get()
|
||||
return &cert, nil
|
||||
}
|
||||
}
|
||||
|
||||
var caDir string
|
||||
if opts != nil {
|
||||
caDir = opts.CADir
|
||||
}
|
||||
conf.RootCAs, err = certs.GetRootCAs(env.Get(EnvKESServerCA, caDir))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := kes.NewClientWithConfig("", conf)
|
||||
client.Endpoints = endpoints
|
||||
|
||||
// Keep the default key in the KES cache to prevent availability issues
|
||||
// when MinIO restarts
|
||||
go func() {
|
||||
timer := time.NewTicker(10 * time.Second)
|
||||
defer timer.Stop()
|
||||
defaultKey := env.Get(EnvKESDefaultKey, "")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
client.DescribeKey(ctx, defaultKey)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return &KMS{
|
||||
Type: MinKES,
|
||||
DefaultKey: env.Get(EnvKESDefaultKey, ""),
|
||||
conn: &kesConn{
|
||||
defaultKeyID: env.Get(EnvKESDefaultKey, ""),
|
||||
client: client,
|
||||
},
|
||||
latencyBuckets: defaultLatencyBuckets,
|
||||
latency: make([]atomic.Uint64, len(defaultLatencyBuckets)),
|
||||
}, nil
|
||||
default:
|
||||
var s string
|
||||
if lookup(EnvKMSSecretKeyFile) {
|
||||
b, err := os.ReadFile(env.Get(EnvKMSSecretKeyFile, ""))
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if os.IsNotExist(err) {
|
||||
// Relative path where "/run/secrets" is the default docker path for secrets
|
||||
b, err = os.ReadFile(filepath.Join("/run/secrets", env.Get(EnvKMSSecretKeyFile, "")))
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s = string(b)
|
||||
} else {
|
||||
s = env.Get(EnvKMSSecretKey, "")
|
||||
}
|
||||
return ParseSecretKey(s)
|
||||
}
|
||||
}
|
||||
|
||||
// IsPresent reports whether a KMS configuration is present.
|
||||
// It returns an error if multiple KMS configurations are
|
||||
// present or if one configuration is incomplete.
|
||||
func IsPresent() (bool, error) {
|
||||
// isPresent reports whether at least one of the
|
||||
// given env. variables is present.
|
||||
isPresent := func(vars ...string) bool {
|
||||
for _, v := range vars {
|
||||
if _, ok := os.LookupEnv(v); ok {
|
||||
return ok
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// First, check which KMS/KES env. variables are present.
|
||||
// Only one set, either KMS, KES or static key must be
|
||||
// present.
|
||||
kmsPresent := isPresent(
|
||||
EnvKMSEndpoint,
|
||||
EnvKMSEnclave,
|
||||
EnvKMSAPIKey,
|
||||
EnvKMSDefaultKey,
|
||||
)
|
||||
kesPresent := isPresent(
|
||||
EnvKESEndpoint,
|
||||
EnvKESDefaultKey,
|
||||
EnvKESAPIKey,
|
||||
EnvKESClientKey,
|
||||
EnvKESClientCert,
|
||||
EnvKESClientPassword,
|
||||
EnvKESServerCA,
|
||||
)
|
||||
// We have to handle a special case for MINIO_KMS_SECRET_KEY and
|
||||
// MINIO_KMS_SECRET_KEY_FILE. The docker image always sets the
|
||||
// MINIO_KMS_SECRET_KEY_FILE - either to the argument passed to
|
||||
// the container or to a default string (e.g. "minio_master_key").
|
||||
//
|
||||
// We have to distinguish a explicit config from an implicit. Hence,
|
||||
// we unset the env. vars if they are set but empty or contain a path
|
||||
// which does not exist. The downside of this check is that if
|
||||
// MINIO_KMS_SECRET_KEY_FILE is set to a path that does not exist,
|
||||
// the server does not complain and start without a KMS config.
|
||||
//
|
||||
// Until the container image changes, this behavior has to be preserved.
|
||||
if isPresent(EnvKMSSecretKey) && os.Getenv(EnvKMSSecretKey) == "" {
|
||||
os.Unsetenv(EnvKMSSecretKey)
|
||||
}
|
||||
if isPresent(EnvKMSSecretKeyFile) {
|
||||
if filename := os.Getenv(EnvKMSSecretKeyFile); filename == "" {
|
||||
os.Unsetenv(EnvKMSSecretKeyFile)
|
||||
} else if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||
os.Unsetenv(EnvKMSSecretKeyFile)
|
||||
}
|
||||
}
|
||||
// Now, the static key env. vars are only present if they contain explicit
|
||||
// values.
|
||||
staticKeyPresent := isPresent(EnvKMSSecretKey, EnvKMSSecretKeyFile)
|
||||
|
||||
switch {
|
||||
case kmsPresent && kesPresent:
|
||||
return false, errors.New("kms: configuration for MinIO KMS and MinIO KES is present")
|
||||
case kmsPresent && staticKeyPresent:
|
||||
return false, errors.New("kms: configuration for MinIO KMS and static KMS key is present")
|
||||
case kesPresent && staticKeyPresent:
|
||||
return false, errors.New("kms: configuration for MinIO KES and static KMS key is present")
|
||||
}
|
||||
|
||||
// Next, we check that all required configuration for the concrete
|
||||
// KMS is present.
|
||||
// For example, the MinIO KMS requires an endpoint or a list of
|
||||
// endpoints and authentication credentials. However, a path to
|
||||
// CA certificates is optional.
|
||||
switch {
|
||||
default:
|
||||
return false, nil // No KMS config present
|
||||
case kmsPresent:
|
||||
if !isPresent(EnvKMSEndpoint) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KMS: missing '%s'", EnvKMSEndpoint)
|
||||
}
|
||||
if !isPresent(EnvKMSEnclave) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KMS: missing '%s'", EnvKMSEnclave)
|
||||
}
|
||||
if !isPresent(EnvKMSDefaultKey) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KMS: missing '%s'", EnvKMSDefaultKey)
|
||||
}
|
||||
if !isPresent(EnvKMSAPIKey) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KMS: missing '%s'", EnvKMSAPIKey)
|
||||
}
|
||||
return true, nil
|
||||
case staticKeyPresent:
|
||||
if isPresent(EnvKMSSecretKey) && isPresent(EnvKMSSecretKeyFile) {
|
||||
return false, fmt.Errorf("kms: invalid configuration for static KMS key: '%s' and '%s' are present", EnvKMSSecretKey, EnvKMSSecretKeyFile)
|
||||
}
|
||||
return true, nil
|
||||
case kesPresent:
|
||||
if !isPresent(EnvKESEndpoint) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KES: missing '%s'", EnvKESEndpoint)
|
||||
}
|
||||
if !isPresent(EnvKESDefaultKey) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KES: missing '%s'", EnvKESDefaultKey)
|
||||
}
|
||||
|
||||
if isPresent(EnvKESClientKey, EnvKESClientCert, EnvKESClientPassword) {
|
||||
if isPresent(EnvKESAPIKey) {
|
||||
return false, fmt.Errorf("kms: invalid configuration for MinIO KES: '%s' and client certificate is present", EnvKESAPIKey)
|
||||
}
|
||||
if !isPresent(EnvKESClientCert) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KES: missing '%s'", EnvKESClientCert)
|
||||
}
|
||||
if !isPresent(EnvKESClientKey) {
|
||||
return false, fmt.Errorf("kms: incomplete configuration for MinIO KES: missing '%s'", EnvKESClientKey)
|
||||
}
|
||||
} else if !isPresent(EnvKESAPIKey) {
|
||||
return false, errors.New("kms: incomplete configuration for MinIO KES: missing authentication method")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func expandEndpoints(s string) ([]string, error) {
|
||||
var endpoints []string
|
||||
for endpoint := range strings.SplitSeq(s, ",") {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if endpoint == "" {
|
||||
continue
|
||||
}
|
||||
if !ellipses.HasEllipses(endpoint) {
|
||||
endpoints = append(endpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
|
||||
pattern, err := ellipses.FindEllipsesPatterns(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("kms: invalid endpoint '%s': %v", endpoint, err)
|
||||
}
|
||||
for _, p := range pattern.Expand() {
|
||||
endpoints = append(endpoints, strings.Join(p, ""))
|
||||
}
|
||||
}
|
||||
return endpoints, nil
|
||||
}
|
||||
105
internal/kms/config_test.go
Normal file
105
internal/kms/config_test.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||
//
|
||||
// # This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsPresent(t *testing.T) {
|
||||
for i, test := range isPresentTests {
|
||||
os.Clearenv()
|
||||
for k, v := range test.Env {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
ok, err := IsPresent()
|
||||
if err != nil && !test.ShouldFail {
|
||||
t.Fatalf("Test %d: %v", i, err)
|
||||
}
|
||||
if err == nil && test.ShouldFail {
|
||||
t.Fatalf("Test %d: should have failed but succeeded", i)
|
||||
}
|
||||
|
||||
if !test.ShouldFail && ok != test.IsPresent {
|
||||
t.Fatalf("Test %d: reported that KMS present=%v - want present=%v", i, ok, test.IsPresent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isPresentTests = []struct {
|
||||
Env map[string]string
|
||||
IsPresent bool
|
||||
ShouldFail bool
|
||||
}{
|
||||
{Env: map[string]string{}}, // 0
|
||||
{ // 1
|
||||
Env: map[string]string{
|
||||
EnvKMSSecretKey: "minioy-default-key:6jEQjjMh8iPq8/gqgb4eMDIZFOtPACIsr9kO+vx8JFs=",
|
||||
},
|
||||
IsPresent: true,
|
||||
},
|
||||
{ // 2
|
||||
Env: map[string]string{
|
||||
EnvKMSEndpoint: "https://127.0.0.1:7373",
|
||||
EnvKMSDefaultKey: "minio-key",
|
||||
EnvKMSEnclave: "demo",
|
||||
EnvKMSAPIKey: "k1:MBDtmC9ZAf3Wi4-oGglgKx_6T1jwJfct1IC15HOxetg",
|
||||
},
|
||||
IsPresent: true,
|
||||
},
|
||||
{ // 3
|
||||
Env: map[string]string{
|
||||
EnvKESEndpoint: "https://127.0.0.1:7373",
|
||||
EnvKESDefaultKey: "minio-key",
|
||||
EnvKESAPIKey: "kes:v1:AGtR4PvKXNjz+/MlBX2Djg0qxwS3C4OjoDzsuFSQr82e",
|
||||
},
|
||||
IsPresent: true,
|
||||
},
|
||||
{ // 4
|
||||
Env: map[string]string{
|
||||
EnvKESEndpoint: "https://127.0.0.1:7373",
|
||||
EnvKESDefaultKey: "minio-key",
|
||||
EnvKESClientKey: "/tmp/client.key",
|
||||
EnvKESClientCert: "/tmp/client.crt",
|
||||
},
|
||||
IsPresent: true,
|
||||
},
|
||||
{ // 5
|
||||
Env: map[string]string{
|
||||
EnvKMSEndpoint: "https://127.0.0.1:7373",
|
||||
EnvKESEndpoint: "https://127.0.0.1:7373",
|
||||
},
|
||||
ShouldFail: true,
|
||||
},
|
||||
{ // 6
|
||||
Env: map[string]string{
|
||||
EnvKMSEndpoint: "https://127.0.0.1:7373",
|
||||
EnvKMSSecretKey: "minioy-default-key:6jEQjjMh8iPq8/gqgb4eMDIZFOtPACIsr9kO+vx8JFs=",
|
||||
},
|
||||
ShouldFail: true,
|
||||
},
|
||||
{ // 7
|
||||
Env: map[string]string{
|
||||
EnvKMSEnclave: "foo",
|
||||
EnvKESServerCA: "/etc/minio/certs",
|
||||
},
|
||||
ShouldFail: true,
|
||||
},
|
||||
}
|
||||
167
internal/kms/conn.go
Normal file
167
internal/kms/conn.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
// conn represents a connection to a KMS implementation.
|
||||
// It's implemented by the MinKMS and KES client wrappers
|
||||
// and the static / single key KMS.
|
||||
type conn interface {
|
||||
// Version returns version information about the KMS.
|
||||
//
|
||||
// TODO(aead): refactor this API call. It does not account
|
||||
// for multiple endpoints.
|
||||
Version(context.Context) (string, error)
|
||||
|
||||
// APIs returns a list of APIs supported by the KMS server.
|
||||
//
|
||||
// TODO(aead): remove this API call. It's hardly useful.
|
||||
APIs(context.Context) ([]madmin.KMSAPI, error)
|
||||
|
||||
// Stat returns the current KMS status.
|
||||
Status(context.Context) (map[string]madmin.ItemState, error)
|
||||
|
||||
// CreateKey creates a new key at the KMS with the given key ID.
|
||||
CreateKey(context.Context, *CreateKeyRequest) error
|
||||
|
||||
ListKeys(context.Context, *ListRequest) ([]madmin.KMSKeyInfo, string, error)
|
||||
|
||||
// GenerateKey generates a new data encryption key using the
|
||||
// key referenced by the key ID.
|
||||
//
|
||||
// The KMS may use a default key if the key ID is empty.
|
||||
// GenerateKey returns an error if the referenced key does
|
||||
// not exist.
|
||||
//
|
||||
// The context is associated and tied to the generated DEK.
|
||||
// The same context must be provided when the generated key
|
||||
// should be decrypted. Therefore, it is the callers
|
||||
// responsibility to remember the corresponding context for
|
||||
// a particular DEK. The context may be nil.
|
||||
GenerateKey(context.Context, *GenerateKeyRequest) (DEK, error)
|
||||
|
||||
// DecryptKey decrypts the ciphertext with the key referenced
|
||||
// by the key ID. The context must match the context value
|
||||
// used to generate the ciphertext.
|
||||
Decrypt(context.Context, *DecryptRequest) ([]byte, error)
|
||||
|
||||
// MAC generates the checksum of the given req.Message using the key
|
||||
// with the req.Name at the KMS.
|
||||
MAC(context.Context, *MACRequest) ([]byte, error)
|
||||
}
|
||||
|
||||
var ( // compiler checks
|
||||
_ conn = (*kmsConn)(nil)
|
||||
_ conn = (*kesConn)(nil)
|
||||
_ conn = secretKey{}
|
||||
)
|
||||
|
||||
// Supported KMS types
|
||||
const (
|
||||
MinKMS Type = iota + 1 // MinIO KMS
|
||||
MinKES // MinIO MinKES
|
||||
Builtin // Builtin single key KMS implementation
|
||||
)
|
||||
|
||||
// Type identifies the KMS type.
|
||||
type Type uint
|
||||
|
||||
// String returns the Type's string representation
|
||||
func (t Type) String() string {
|
||||
switch t {
|
||||
case MinKMS:
|
||||
return "MinIO KMS"
|
||||
case MinKES:
|
||||
return "MinIO KES"
|
||||
case Builtin:
|
||||
return "MinIO builtin"
|
||||
default:
|
||||
return "!INVALID:" + strconv.Itoa(int(t))
|
||||
}
|
||||
}
|
||||
|
||||
// Status describes the current state of a KMS.
|
||||
type Status struct {
|
||||
Online map[string]struct{}
|
||||
Offline map[string]Error
|
||||
}
|
||||
|
||||
// DEK is a data encryption key. It consists of a
|
||||
// plaintext-ciphertext pair and the ID of the key
|
||||
// used to generate the ciphertext.
|
||||
//
|
||||
// The plaintext can be used for cryptographic
|
||||
// operations - like encrypting some data. The
|
||||
// ciphertext is the encrypted version of the
|
||||
// plaintext data and can be stored on untrusted
|
||||
// storage.
|
||||
type DEK struct {
|
||||
KeyID string // Name of the master key
|
||||
Version int // Version of the master key (MinKMS only)
|
||||
Plaintext []byte // Paintext of the data encryption key
|
||||
Ciphertext []byte // Ciphertext of the data encryption key
|
||||
}
|
||||
|
||||
var (
|
||||
_ encoding.TextMarshaler = (*DEK)(nil)
|
||||
_ encoding.TextUnmarshaler = (*DEK)(nil)
|
||||
)
|
||||
|
||||
// MarshalText encodes the DEK's key ID and ciphertext
|
||||
// as JSON.
|
||||
func (d DEK) MarshalText() ([]byte, error) {
|
||||
type JSON struct {
|
||||
KeyID string `json:"keyid"`
|
||||
Version uint32 `json:"version,omitempty"`
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
}
|
||||
return json.Marshal(JSON{
|
||||
KeyID: d.KeyID,
|
||||
Version: uint32(d.Version),
|
||||
Ciphertext: d.Ciphertext,
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalText tries to decode text as JSON representation
|
||||
// of a DEK and sets DEK's key ID and ciphertext to the
|
||||
// decoded values.
|
||||
//
|
||||
// It sets DEK's plaintext to nil.
|
||||
func (d *DEK) UnmarshalText(text []byte) error {
|
||||
type JSON struct {
|
||||
KeyID string `json:"keyid"`
|
||||
Version uint32 `json:"version"`
|
||||
Ciphertext []byte `json:"ciphertext"`
|
||||
}
|
||||
var v JSON
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
if err := json.Unmarshal(text, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
d.KeyID, d.Version, d.Plaintext, d.Ciphertext = v.KeyID, int(v.Version), nil, v.Ciphertext
|
||||
return nil
|
||||
}
|
||||
259
internal/kms/context.go
Normal file
259
internal/kms/context.go
Normal file
@@ -0,0 +1,259 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Context is a set of key-value pairs that
|
||||
// are associated with a generate data encryption
|
||||
// key (DEK).
|
||||
//
|
||||
// A KMS implementation may bind the context to the
|
||||
// generated DEK such that the same context must be
|
||||
// provided when decrypting an encrypted DEK.
|
||||
type Context map[string]string
|
||||
|
||||
// MarshalText returns a canonical text representation of
|
||||
// the Context.
|
||||
|
||||
// MarshalText sorts the context keys and writes the sorted
|
||||
// key-value pairs as canonical JSON object. The sort order
|
||||
// is based on the un-escaped keys. It never returns an error.
|
||||
func (c Context) MarshalText() ([]byte, error) {
|
||||
if len(c) == 0 {
|
||||
return []byte{'{', '}'}, nil
|
||||
}
|
||||
|
||||
// Pre-allocate a buffer - 128 bytes is an arbitrary
|
||||
// heuristic value that seems like a good starting size.
|
||||
b := bytes.NewBuffer(make([]byte, 0, 128))
|
||||
if len(c) == 1 {
|
||||
for k, v := range c {
|
||||
b.WriteString(`{"`)
|
||||
escapeStringJSON(b, k)
|
||||
b.WriteString(`":"`)
|
||||
escapeStringJSON(b, v)
|
||||
b.WriteString(`"}`)
|
||||
}
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
sortedKeys := make([]string, 0, len(c))
|
||||
for k := range c {
|
||||
sortedKeys = append(sortedKeys, k)
|
||||
}
|
||||
sort.Strings(sortedKeys)
|
||||
|
||||
b.WriteByte('{')
|
||||
for i, k := range sortedKeys {
|
||||
b.WriteByte('"')
|
||||
escapeStringJSON(b, k)
|
||||
b.WriteString(`":"`)
|
||||
escapeStringJSON(b, c[k])
|
||||
b.WriteByte('"')
|
||||
if i < len(sortedKeys)-1 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
}
|
||||
b.WriteByte('}')
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
// Adapted from Go stdlib.
|
||||
|
||||
var hexTable = "0123456789abcdef"
|
||||
|
||||
// escapeStringJSON will escape a string for JSON and write it to dst.
|
||||
func escapeStringJSON(dst *bytes.Buffer, s string) {
|
||||
start := 0
|
||||
for i := 0; i < len(s); {
|
||||
if b := s[i]; b < utf8.RuneSelf {
|
||||
if htmlSafeSet[b] {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if start < i {
|
||||
dst.WriteString(s[start:i])
|
||||
}
|
||||
dst.WriteByte('\\')
|
||||
switch b {
|
||||
case '\\', '"':
|
||||
dst.WriteByte(b)
|
||||
case '\n':
|
||||
dst.WriteByte('n')
|
||||
case '\r':
|
||||
dst.WriteByte('r')
|
||||
case '\t':
|
||||
dst.WriteByte('t')
|
||||
default:
|
||||
// This encodes bytes < 0x20 except for \t, \n and \r.
|
||||
// If escapeHTML is set, it also escapes <, >, and &
|
||||
// because they can lead to security holes when
|
||||
// user-controlled strings are rendered into JSON
|
||||
// and served to some browsers.
|
||||
dst.WriteString(`u00`)
|
||||
dst.WriteByte(hexTable[b>>4])
|
||||
dst.WriteByte(hexTable[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
c, size := utf8.DecodeRuneInString(s[i:])
|
||||
if c == utf8.RuneError && size == 1 {
|
||||
if start < i {
|
||||
dst.WriteString(s[start:i])
|
||||
}
|
||||
dst.WriteString(`\ufffd`)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
// U+2028 is LINE SEPARATOR.
|
||||
// U+2029 is PARAGRAPH SEPARATOR.
|
||||
// They are both technically valid characters in JSON strings,
|
||||
// but don't work in JSONP, which has to be evaluated as JavaScript,
|
||||
// and can lead to security holes there. It is valid JSON to
|
||||
// escape them, so we do so unconditionally.
|
||||
// See http://timelessrepo.com/json-isnt-a-javascript-subset for discussion.
|
||||
if c == '\u2028' || c == '\u2029' {
|
||||
if start < i {
|
||||
dst.WriteString(s[start:i])
|
||||
}
|
||||
dst.WriteString(`\u202`)
|
||||
dst.WriteByte(hexTable[c&0xF])
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
}
|
||||
if start < len(s) {
|
||||
dst.WriteString(s[start:])
|
||||
}
|
||||
}
|
||||
|
||||
// htmlSafeSet holds the value true if the ASCII character with the given
|
||||
// array position can be safely represented inside a JSON string, embedded
|
||||
// inside of HTML <script> tags, without any additional escaping.
|
||||
//
|
||||
// All values are true except for the ASCII control characters (0-31), the
|
||||
// double quote ("), the backslash character ("\"), HTML opening and closing
|
||||
// tags ("<" and ">"), and the ampersand ("&").
|
||||
var htmlSafeSet = [utf8.RuneSelf]bool{
|
||||
' ': true,
|
||||
'!': true,
|
||||
'"': false,
|
||||
'#': true,
|
||||
'$': true,
|
||||
'%': true,
|
||||
'&': false,
|
||||
'\'': true,
|
||||
'(': true,
|
||||
')': true,
|
||||
'*': true,
|
||||
'+': true,
|
||||
',': true,
|
||||
'-': true,
|
||||
'.': true,
|
||||
'/': true,
|
||||
'0': true,
|
||||
'1': true,
|
||||
'2': true,
|
||||
'3': true,
|
||||
'4': true,
|
||||
'5': true,
|
||||
'6': true,
|
||||
'7': true,
|
||||
'8': true,
|
||||
'9': true,
|
||||
':': true,
|
||||
';': true,
|
||||
'<': false,
|
||||
'=': true,
|
||||
'>': false,
|
||||
'?': true,
|
||||
'@': true,
|
||||
'A': true,
|
||||
'B': true,
|
||||
'C': true,
|
||||
'D': true,
|
||||
'E': true,
|
||||
'F': true,
|
||||
'G': true,
|
||||
'H': true,
|
||||
'I': true,
|
||||
'J': true,
|
||||
'K': true,
|
||||
'L': true,
|
||||
'M': true,
|
||||
'N': true,
|
||||
'O': true,
|
||||
'P': true,
|
||||
'Q': true,
|
||||
'R': true,
|
||||
'S': true,
|
||||
'T': true,
|
||||
'U': true,
|
||||
'V': true,
|
||||
'W': true,
|
||||
'X': true,
|
||||
'Y': true,
|
||||
'Z': true,
|
||||
'[': true,
|
||||
'\\': false,
|
||||
']': true,
|
||||
'^': true,
|
||||
'_': true,
|
||||
'`': true,
|
||||
'a': true,
|
||||
'b': true,
|
||||
'c': true,
|
||||
'd': true,
|
||||
'e': true,
|
||||
'f': true,
|
||||
'g': true,
|
||||
'h': true,
|
||||
'i': true,
|
||||
'j': true,
|
||||
'k': true,
|
||||
'l': true,
|
||||
'm': true,
|
||||
'n': true,
|
||||
'o': true,
|
||||
'p': true,
|
||||
'q': true,
|
||||
'r': true,
|
||||
's': true,
|
||||
't': true,
|
||||
'u': true,
|
||||
'v': true,
|
||||
'w': true,
|
||||
'x': true,
|
||||
'y': true,
|
||||
'z': true,
|
||||
'{': true,
|
||||
'|': true,
|
||||
'}': true,
|
||||
'~': true,
|
||||
'\u007f': true,
|
||||
}
|
||||
79
internal/kms/dek_test.go
Normal file
79
internal/kms/dek_test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var dekEncodeDecodeTests = []struct {
|
||||
Key DEK
|
||||
}{
|
||||
{
|
||||
Key: DEK{},
|
||||
},
|
||||
{
|
||||
Key: DEK{
|
||||
Plaintext: nil,
|
||||
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: DEK{
|
||||
Plaintext: mustDecodeB64("GM2UvLXp/X8lzqq0mibFC0LayDCGlmTHQhYLj7qAy7Q="),
|
||||
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: DEK{
|
||||
Version: 3,
|
||||
Plaintext: mustDecodeB64("GM2UvLXp/X8lzqq0mibFC0LayDCGlmTHQhYLj7qAy7Q="),
|
||||
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestEncodeDecodeDEK(t *testing.T) {
|
||||
for i, test := range dekEncodeDecodeTests {
|
||||
text, err := test.Key.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to marshal DEK: %v", i, err)
|
||||
}
|
||||
|
||||
var key DEK
|
||||
if err = key.UnmarshalText(text); err != nil {
|
||||
t.Fatalf("Test %d: failed to unmarshal DEK: %v", i, err)
|
||||
}
|
||||
if key.Plaintext != nil {
|
||||
t.Fatalf("Test %d: unmarshaled DEK contains non-nil plaintext", i)
|
||||
}
|
||||
if !bytes.Equal(key.Ciphertext, test.Key.Ciphertext) {
|
||||
t.Fatalf("Test %d: ciphertext mismatch: got %x - want %x", i, key.Ciphertext, test.Key.Ciphertext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustDecodeB64(s string) []byte {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
128
internal/kms/errors.go
Normal file
128
internal/kms/errors.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPermission is an error returned by the KMS when it has not
|
||||
// enough permissions to perform the operation.
|
||||
ErrPermission = Error{
|
||||
Code: http.StatusForbidden,
|
||||
APICode: "kms:NotAuthorized",
|
||||
Err: "insufficient permissions to perform KMS operation",
|
||||
}
|
||||
|
||||
// ErrKeyExists is an error returned by the KMS when trying to
|
||||
// create a key that already exists.
|
||||
ErrKeyExists = Error{
|
||||
Code: http.StatusConflict,
|
||||
APICode: "kms:KeyAlreadyExists",
|
||||
Err: "key with given key ID already exits",
|
||||
}
|
||||
|
||||
// ErrKeyNotFound is an error returned by the KMS when trying to
|
||||
// use a key that does not exist.
|
||||
ErrKeyNotFound = Error{
|
||||
Code: http.StatusNotFound,
|
||||
APICode: "kms:KeyNotFound",
|
||||
Err: "key with given key ID does not exist",
|
||||
}
|
||||
|
||||
// ErrDecrypt is an error returned by the KMS when the decryption
|
||||
// of a ciphertext failed.
|
||||
ErrDecrypt = Error{
|
||||
Code: http.StatusBadRequest,
|
||||
APICode: "kms:InvalidCiphertextException",
|
||||
Err: "failed to decrypt ciphertext",
|
||||
}
|
||||
|
||||
// ErrNotSupported is an error returned by the KMS when the requested
|
||||
// functionality is not supported by the KMS service.
|
||||
ErrNotSupported = Error{
|
||||
Code: http.StatusNotImplemented,
|
||||
APICode: "kms:NotSupported",
|
||||
Err: "requested functionality is not supported",
|
||||
}
|
||||
)
|
||||
|
||||
// Error is a KMS error that can be translated into an S3 API error.
|
||||
//
|
||||
// It does not implement the standard error Unwrap interface for
|
||||
// better error log messages.
|
||||
type Error struct {
|
||||
Code int // The HTTP status code returned to the client
|
||||
APICode string // The API error code identifying the error
|
||||
Err string // The error message returned to the client
|
||||
Cause error // Optional, lower level error cause.
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
if e.Cause == nil {
|
||||
return e.Err
|
||||
}
|
||||
return fmt.Sprintf("%s: %v", e.Err, e.Cause)
|
||||
}
|
||||
|
||||
func errKeyCreationFailed(err error) Error {
|
||||
return Error{
|
||||
Code: http.StatusInternalServerError,
|
||||
APICode: "kms:KeyCreationFailed",
|
||||
Err: "failed to create KMS key",
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
func errKeyDeletionFailed(err error) Error {
|
||||
return Error{
|
||||
Code: http.StatusInternalServerError,
|
||||
APICode: "kms:KeyDeletionFailed",
|
||||
Err: "failed to delete KMS key",
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
func errListingKeysFailed(err error) Error {
|
||||
return Error{
|
||||
Code: http.StatusInternalServerError,
|
||||
APICode: "kms:KeyListingFailed",
|
||||
Err: "failed to list keys at the KMS",
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
func errKeyGenerationFailed(err error) Error {
|
||||
return Error{
|
||||
Code: http.StatusInternalServerError,
|
||||
APICode: "kms:KeyGenerationFailed",
|
||||
Err: "failed to generate data key with KMS key",
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
func errDecryptionFailed(err error) Error {
|
||||
return Error{
|
||||
Code: http.StatusInternalServerError,
|
||||
APICode: "kms:DecryptionFailed",
|
||||
Err: "failed to decrypt ciphertext with KMS key",
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
270
internal/kms/kes.go
Normal file
270
internal/kms/kes.go
Normal file
@@ -0,0 +1,270 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/kms-go/kes"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
type kesConn struct {
|
||||
defaultKeyID string
|
||||
client *kes.Client
|
||||
}
|
||||
|
||||
func (c *kesConn) Version(ctx context.Context) (string, error) {
|
||||
return c.client.Version(ctx)
|
||||
}
|
||||
|
||||
func (c *kesConn) APIs(ctx context.Context) ([]madmin.KMSAPI, error) {
|
||||
APIs, err := c.client.APIs(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrNotAllowed) {
|
||||
return nil, ErrPermission
|
||||
}
|
||||
return nil, Error{
|
||||
Code: http.StatusInternalServerError,
|
||||
APICode: "kms:InternalError",
|
||||
Err: "failed to list KMS APIs",
|
||||
Cause: err,
|
||||
}
|
||||
}
|
||||
|
||||
list := make([]madmin.KMSAPI, 0, len(APIs))
|
||||
for _, api := range APIs {
|
||||
list = append(list, madmin.KMSAPI{
|
||||
Method: api.Method,
|
||||
Path: api.Path,
|
||||
MaxBody: api.MaxBody,
|
||||
Timeout: int64(api.Timeout.Truncate(time.Second).Seconds()),
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// Stat returns the current KES status containing a
|
||||
// list of KES endpoints and the default key ID.
|
||||
func (c *kesConn) Status(ctx context.Context) (map[string]madmin.ItemState, error) {
|
||||
if len(c.client.Endpoints) == 1 {
|
||||
if _, err := c.client.Status(ctx); err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
if errors.Is(err, kes.ErrNotAllowed) {
|
||||
return nil, ErrPermission
|
||||
}
|
||||
|
||||
return map[string]madmin.ItemState{
|
||||
c.client.Endpoints[0]: madmin.ItemOffline,
|
||||
}, nil
|
||||
}
|
||||
return map[string]madmin.ItemState{
|
||||
c.client.Endpoints[0]: madmin.ItemOnline,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Endpoint string
|
||||
ItemState madmin.ItemState
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
results := make([]Result, len(c.client.Endpoints))
|
||||
for i := range c.client.Endpoints {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
|
||||
client := kes.Client{
|
||||
Endpoints: []string{c.client.Endpoints[i]},
|
||||
HTTPClient: c.client.HTTPClient,
|
||||
}
|
||||
|
||||
var item madmin.ItemState
|
||||
if _, err := client.Status(ctx); err == nil {
|
||||
item = madmin.ItemOnline
|
||||
} else {
|
||||
item = madmin.ItemOffline
|
||||
}
|
||||
results[i] = Result{
|
||||
Endpoint: c.client.Endpoints[i],
|
||||
ItemState: item,
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
status := make(map[string]madmin.ItemState, len(results))
|
||||
for _, r := range results {
|
||||
if r.ItemState == madmin.ItemOnline {
|
||||
status[r.Endpoint] = madmin.ItemOnline
|
||||
} else {
|
||||
status[r.Endpoint] = madmin.ItemOffline
|
||||
}
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
|
||||
func (c *kesConn) ListKeys(ctx context.Context, req *ListRequest) ([]madmin.KMSKeyInfo, string, error) {
|
||||
names, continueAt, err := c.client.ListKeys(ctx, req.Prefix, req.Limit)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
keyInfos := make([]madmin.KMSKeyInfo, len(names))
|
||||
for i := range names {
|
||||
keyInfos[i].Name = names[i]
|
||||
}
|
||||
return keyInfos, continueAt, nil
|
||||
}
|
||||
|
||||
// CreateKey tries to create a new key at the KMS with the
|
||||
// given key ID.
|
||||
//
|
||||
// If the a key with the same keyID already exists then
|
||||
// CreateKey returns kes.ErrKeyExists.
|
||||
func (c *kesConn) CreateKey(ctx context.Context, req *CreateKeyRequest) error {
|
||||
if err := c.client.CreateKey(ctx, req.Name); err != nil {
|
||||
if errors.Is(err, kes.ErrKeyExists) {
|
||||
return ErrKeyExists
|
||||
}
|
||||
if errors.Is(err, kes.ErrNotAllowed) {
|
||||
return ErrPermission
|
||||
}
|
||||
return errKeyCreationFailed(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteKey deletes a key at the KMS with the given key ID.
|
||||
// Please note that is a dangerous operation.
|
||||
// Once a key has been deleted all data that has been encrypted with it cannot be decrypted
|
||||
// anymore, and therefore, is lost.
|
||||
func (c *kesConn) DeleteKey(ctx context.Context, req *DeleteKeyRequest) error {
|
||||
if err := c.client.DeleteKey(ctx, req.Name); err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
if errors.Is(err, kes.ErrNotAllowed) {
|
||||
return ErrPermission
|
||||
}
|
||||
return errKeyDeletionFailed(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateKey generates a new data encryption key using
|
||||
// the key at the KES server referenced by the key ID.
|
||||
//
|
||||
// The default key ID will be used if keyID is empty.
|
||||
//
|
||||
// The context is associated and tied to the generated DEK.
|
||||
// The same context must be provided when the generated
|
||||
// key should be decrypted.
|
||||
func (c *kesConn) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) {
|
||||
aad, err := req.AssociatedData.MarshalText()
|
||||
if err != nil {
|
||||
return DEK{}, err
|
||||
}
|
||||
|
||||
name := req.Name
|
||||
if name == "" {
|
||||
name = c.defaultKeyID
|
||||
}
|
||||
|
||||
dek, err := c.client.GenerateKey(ctx, name, aad)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
return DEK{}, ErrKeyNotFound
|
||||
}
|
||||
if errors.Is(err, kes.ErrNotAllowed) {
|
||||
return DEK{}, ErrPermission
|
||||
}
|
||||
return DEK{}, errKeyGenerationFailed(err)
|
||||
}
|
||||
return DEK{
|
||||
KeyID: name,
|
||||
Plaintext: dek.Plaintext,
|
||||
Ciphertext: dek.Ciphertext,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ImportKey imports a cryptographic key into the KMS.
|
||||
func (c *kesConn) ImportKey(ctx context.Context, keyID string, bytes []byte) error {
|
||||
return c.client.ImportKey(ctx, keyID, &kes.ImportKeyRequest{
|
||||
Key: bytes,
|
||||
})
|
||||
}
|
||||
|
||||
// EncryptKey Encrypts and authenticates a (small) plaintext with the cryptographic key
|
||||
// The plaintext must not exceed 1 MB
|
||||
func (c *kesConn) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) {
|
||||
ctxBytes, err := ctx.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.client.Encrypt(context.Background(), keyID, plaintext, ctxBytes)
|
||||
}
|
||||
|
||||
// DecryptKey decrypts the ciphertext with the key at the KES
|
||||
// server referenced by the key ID. The context must match the
|
||||
// context value used to generate the ciphertext.
|
||||
func (c *kesConn) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) {
|
||||
aad, err := req.AssociatedData.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plaintext, err := c.client.Decrypt(context.Background(), req.Name, req.Ciphertext, aad)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
if errors.Is(err, kes.ErrDecrypt) {
|
||||
return nil, ErrDecrypt
|
||||
}
|
||||
if errors.Is(err, kes.ErrNotAllowed) {
|
||||
return nil, ErrPermission
|
||||
}
|
||||
return nil, errDecryptionFailed(err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// MAC generates the checksum of the given req.Message using the key
|
||||
// with the req.Name at the KMS.
|
||||
func (c *kesConn) MAC(ctx context.Context, req *MACRequest) ([]byte, error) {
|
||||
mac, err := c.client.HMAC(context.Background(), req.Name, req.Message)
|
||||
if err != nil {
|
||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
if errors.Is(err, kes.ErrNotAllowed) {
|
||||
return nil, ErrPermission
|
||||
}
|
||||
if kErr, ok := err.(kes.Error); ok && kErr.Status() == http.StatusNotImplemented {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
420
internal/kms/kms.go
Normal file
420
internal/kms/kms.go
Normal file
@@ -0,0 +1,420 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/kms-go/kms"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
// ListRequest is a structure containing fields
|
||||
// and options for listing keys.
|
||||
type ListRequest struct {
|
||||
// Prefix is an optional prefix for filtering names.
|
||||
// A list operation only returns elements that match
|
||||
// this prefix.
|
||||
// An empty prefix matches any value.
|
||||
Prefix string
|
||||
|
||||
// ContinueAt is the name of the element from where
|
||||
// a listing should continue. It allows paginated
|
||||
// listings.
|
||||
ContinueAt string
|
||||
|
||||
// Limit limits the number of elements returned by
|
||||
// a single list operation. If <= 0, a reasonable
|
||||
// limit is selected automatically.
|
||||
Limit int
|
||||
}
|
||||
|
||||
// CreateKeyRequest is a structure containing fields
|
||||
// and options for creating keys.
|
||||
type CreateKeyRequest struct {
|
||||
// Name is the name of the key that gets created.
|
||||
Name string
|
||||
}
|
||||
|
||||
// DeleteKeyRequest is a structure containing fields
|
||||
// and options for deleting keys.
|
||||
type DeleteKeyRequest struct {
|
||||
// Name is the name of the key that gets deleted.
|
||||
Name string
|
||||
}
|
||||
|
||||
// GenerateKeyRequest is a structure containing fields
|
||||
// and options for generating data keys.
|
||||
type GenerateKeyRequest struct {
|
||||
// Name is the name of the master key used to generate
|
||||
// the data key.
|
||||
Name string
|
||||
|
||||
// AssociatedData is optional data that is cryptographically
|
||||
// associated with the generated data key. The same data
|
||||
// must be provided when decrypting an encrypted data key.
|
||||
//
|
||||
// Typically, associated data is some metadata about the
|
||||
// data key. For example, the name of the object for which
|
||||
// the data key is used.
|
||||
AssociatedData Context
|
||||
}
|
||||
|
||||
// DecryptRequest is a structure containing fields
|
||||
// and options for decrypting data.
|
||||
type DecryptRequest struct {
|
||||
// Name is the name of the master key used decrypt
|
||||
// the ciphertext.
|
||||
Name string
|
||||
|
||||
// Version is the version of the master used for
|
||||
// decryption. If empty, the latest key version
|
||||
// is used.
|
||||
Version int
|
||||
|
||||
// Ciphertext is the encrypted data that gets
|
||||
// decrypted.
|
||||
Ciphertext []byte
|
||||
|
||||
// AssociatedData is the crypto. associated data.
|
||||
// It must match the data used during encryption
|
||||
// or data key generation.
|
||||
AssociatedData Context
|
||||
}
|
||||
|
||||
// MACRequest is a structure containing fields
|
||||
// and options for generating message authentication
|
||||
// codes (MAC).
|
||||
type MACRequest struct {
|
||||
// Name is the name of the master key used decrypt
|
||||
// the ciphertext.
|
||||
Name string
|
||||
|
||||
Version int
|
||||
|
||||
Message []byte
|
||||
}
|
||||
|
||||
// Metrics is a structure containing KMS metrics.
|
||||
type Metrics struct {
|
||||
ReqOK uint64 `json:"kms_req_success"` // Number of requests that succeeded
|
||||
ReqErr uint64 `json:"kms_req_error"` // Number of requests that failed with a defined error
|
||||
ReqFail uint64 `json:"kms_req_failure"` // Number of requests that failed with an undefined error
|
||||
Latency map[time.Duration]uint64 `json:"kms_resp_time"` // Latency histogram of all requests
|
||||
}
|
||||
|
||||
var defaultLatencyBuckets = []time.Duration{
|
||||
10 * time.Millisecond,
|
||||
50 * time.Millisecond,
|
||||
100 * time.Millisecond,
|
||||
250 * time.Millisecond,
|
||||
500 * time.Millisecond,
|
||||
1000 * time.Millisecond, // 1s
|
||||
1500 * time.Millisecond,
|
||||
3000 * time.Millisecond,
|
||||
5000 * time.Millisecond,
|
||||
10000 * time.Millisecond, // 10s
|
||||
}
|
||||
|
||||
// KMS is a connection to a key management system.
|
||||
// It implements various cryptographic operations,
|
||||
// like data key generation and decryption.
|
||||
type KMS struct {
|
||||
// Type identifies the KMS implementation. Either,
|
||||
// MinKMS, MinKES or Builtin.
|
||||
Type Type
|
||||
|
||||
// The default key, used for generating new data keys
|
||||
// if no explicit GenerateKeyRequest.Name is provided.
|
||||
DefaultKey string
|
||||
|
||||
conn conn // Connection to the KMS
|
||||
|
||||
// Metrics
|
||||
reqOK, reqErr, reqFail atomic.Uint64
|
||||
latencyBuckets []time.Duration // expected to be sorted
|
||||
latency []atomic.Uint64
|
||||
}
|
||||
|
||||
// Version returns version information about the KMS.
|
||||
//
|
||||
// TODO(aead): refactor this API call since it does not account
|
||||
// for multiple KMS/KES servers.
|
||||
func (k *KMS) Version(ctx context.Context) (string, error) {
|
||||
return k.conn.Version(ctx)
|
||||
}
|
||||
|
||||
// APIs returns a list of KMS server APIs.
|
||||
//
|
||||
// TODO(aead): remove this API since it's hardly useful.
|
||||
func (k *KMS) APIs(ctx context.Context) ([]madmin.KMSAPI, error) {
|
||||
return k.conn.APIs(ctx)
|
||||
}
|
||||
|
||||
// Metrics returns a current snapshot of the KMS metrics.
|
||||
func (k *KMS) Metrics(ctx context.Context) (*Metrics, error) {
|
||||
latency := make(map[time.Duration]uint64, len(k.latencyBuckets))
|
||||
for i, b := range k.latencyBuckets {
|
||||
latency[b] = k.latency[i].Load()
|
||||
}
|
||||
|
||||
return &Metrics{
|
||||
ReqOK: k.reqOK.Load(),
|
||||
ReqErr: k.reqErr.Load(),
|
||||
ReqFail: k.reqFail.Load(),
|
||||
Latency: latency,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Status returns status information about the KMS.
|
||||
//
|
||||
// TODO(aead): refactor this API call since it does not account
|
||||
// for multiple KMS/KES servers.
|
||||
func (k *KMS) Status(ctx context.Context) (*madmin.KMSStatus, error) {
|
||||
endpoints, err := k.conn.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &madmin.KMSStatus{
|
||||
Name: k.Type.String(),
|
||||
DefaultKeyID: k.DefaultKey,
|
||||
Endpoints: endpoints,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateKey creates the master key req.Name. It returns
|
||||
// ErrKeyExists if the key already exists.
|
||||
func (k *KMS) CreateKey(ctx context.Context, req *CreateKeyRequest) error {
|
||||
start := time.Now()
|
||||
err := k.conn.CreateKey(ctx, req)
|
||||
k.updateMetrics(err, time.Since(start))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// ListKeys returns a list of keys with metadata and a potential
|
||||
// next name from where to continue a subsequent listing.
|
||||
func (k *KMS) ListKeys(ctx context.Context, req *ListRequest) ([]madmin.KMSKeyInfo, string, error) {
|
||||
if req.Prefix == "*" {
|
||||
req.Prefix = ""
|
||||
}
|
||||
return k.conn.ListKeys(ctx, req)
|
||||
}
|
||||
|
||||
// GenerateKey generates a new data key using the master key req.Name.
|
||||
// It returns ErrKeyNotFound if the key does not exist. If req.Name is
|
||||
// empty, the KMS default key is used.
|
||||
func (k *KMS) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) {
|
||||
if req.Name == "" {
|
||||
req.Name = k.DefaultKey
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
dek, err := k.conn.GenerateKey(ctx, req)
|
||||
k.updateMetrics(err, time.Since(start))
|
||||
|
||||
return dek, err
|
||||
}
|
||||
|
||||
// Decrypt decrypts a ciphertext using the master key req.Name.
|
||||
// It returns ErrKeyNotFound if the key does not exist.
|
||||
func (k *KMS) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) {
|
||||
start := time.Now()
|
||||
plaintext, err := k.conn.Decrypt(ctx, req)
|
||||
k.updateMetrics(err, time.Since(start))
|
||||
|
||||
return plaintext, err
|
||||
}
|
||||
|
||||
// MAC generates the checksum of the given req.Message using the key
|
||||
// with the req.Name at the KMS.
|
||||
func (k *KMS) MAC(ctx context.Context, req *MACRequest) ([]byte, error) {
|
||||
if req.Name == "" {
|
||||
req.Name = k.DefaultKey
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
mac, err := k.conn.MAC(ctx, req)
|
||||
k.updateMetrics(err, time.Since(start))
|
||||
|
||||
return mac, err
|
||||
}
|
||||
|
||||
func (k *KMS) updateMetrics(err error, latency time.Duration) {
|
||||
// First, update the latency histogram
|
||||
// Therefore, find the first bucket that holds the counter for
|
||||
// requests with a latency at least as large as the given request
|
||||
// latency and update its and all subsequent counters.
|
||||
bucket := slices.IndexFunc(k.latencyBuckets, func(b time.Duration) bool { return latency < b })
|
||||
if bucket < 0 {
|
||||
bucket = len(k.latencyBuckets) - 1
|
||||
}
|
||||
for i := bucket; i < len(k.latency); i++ {
|
||||
k.latency[i].Add(1)
|
||||
}
|
||||
|
||||
// Next, update the request counters
|
||||
if err == nil {
|
||||
k.reqOK.Add(1)
|
||||
return
|
||||
}
|
||||
|
||||
var s3Err Error
|
||||
if errors.As(err, &s3Err) && s3Err.Code >= http.StatusInternalServerError {
|
||||
k.reqFail.Add(1)
|
||||
} else {
|
||||
k.reqErr.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
type kmsConn struct {
|
||||
endpoints []string
|
||||
enclave string
|
||||
defaultKey string
|
||||
client *kms.Client
|
||||
}
|
||||
|
||||
func (c *kmsConn) Version(ctx context.Context) (string, error) {
|
||||
resp, err := c.client.Version(ctx, &kms.VersionRequest{})
|
||||
if len(resp) == 0 && err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp[0].Version, nil
|
||||
}
|
||||
|
||||
func (c *kmsConn) APIs(ctx context.Context) ([]madmin.KMSAPI, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
func (c *kmsConn) Status(ctx context.Context) (map[string]madmin.ItemState, error) {
|
||||
stat := make(map[string]madmin.ItemState, len(c.endpoints))
|
||||
resp, err := c.client.Version(ctx, &kms.VersionRequest{})
|
||||
|
||||
for _, r := range resp {
|
||||
stat[r.Host] = madmin.ItemOnline
|
||||
}
|
||||
for _, e := range kms.UnwrapHostErrors(err) {
|
||||
stat[e.Host] = madmin.ItemOffline
|
||||
}
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
func (c *kmsConn) ListKeys(ctx context.Context, req *ListRequest) ([]madmin.KMSKeyInfo, string, error) {
|
||||
resp, err := c.client.ListKeys(ctx, &kms.ListRequest{
|
||||
Enclave: c.enclave,
|
||||
Prefix: req.Prefix,
|
||||
ContinueAt: req.ContinueAt,
|
||||
Limit: req.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, "", errListingKeysFailed(err)
|
||||
}
|
||||
|
||||
keyInfos := make([]madmin.KMSKeyInfo, len(resp.Items))
|
||||
for i, v := range resp.Items {
|
||||
keyInfos[i].Name = v.Name
|
||||
keyInfos[i].CreatedAt = v.CreatedAt
|
||||
keyInfos[i].CreatedBy = v.CreatedBy.String()
|
||||
}
|
||||
return keyInfos, resp.ContinueAt, nil
|
||||
}
|
||||
|
||||
func (c *kmsConn) CreateKey(ctx context.Context, req *CreateKeyRequest) error {
|
||||
if err := c.client.CreateKey(ctx, c.enclave, &kms.CreateKeyRequest{
|
||||
Name: req.Name,
|
||||
}); err != nil {
|
||||
if errors.Is(err, kms.ErrKeyExists) {
|
||||
return ErrKeyExists
|
||||
}
|
||||
if errors.Is(err, kms.ErrPermission) {
|
||||
return ErrPermission
|
||||
}
|
||||
return errKeyCreationFailed(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *kmsConn) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) {
|
||||
aad, err := req.AssociatedData.MarshalText()
|
||||
if err != nil {
|
||||
return DEK{}, err
|
||||
}
|
||||
|
||||
name := req.Name
|
||||
if name == "" {
|
||||
name = c.defaultKey
|
||||
}
|
||||
|
||||
resp, err := c.client.GenerateKey(ctx, c.enclave, &kms.GenerateKeyRequest{
|
||||
Name: name,
|
||||
AssociatedData: aad,
|
||||
Length: 32,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, kms.ErrKeyNotFound) {
|
||||
return DEK{}, ErrKeyNotFound
|
||||
}
|
||||
if errors.Is(err, kms.ErrPermission) {
|
||||
return DEK{}, ErrPermission
|
||||
}
|
||||
return DEK{}, errKeyGenerationFailed(err)
|
||||
}
|
||||
|
||||
return DEK{
|
||||
KeyID: name,
|
||||
Version: resp[0].Version,
|
||||
Plaintext: resp[0].Plaintext,
|
||||
Ciphertext: resp[0].Ciphertext,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *kmsConn) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) {
|
||||
aad, err := req.AssociatedData.MarshalText()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ciphertext, _ := parseCiphertext(req.Ciphertext)
|
||||
resp, err := c.client.Decrypt(ctx, c.enclave, &kms.DecryptRequest{
|
||||
Name: req.Name,
|
||||
Ciphertext: ciphertext,
|
||||
AssociatedData: aad,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, kms.ErrKeyNotFound) {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
if errors.Is(err, kms.ErrPermission) {
|
||||
return nil, ErrPermission
|
||||
}
|
||||
return nil, errDecryptionFailed(err)
|
||||
}
|
||||
return resp[0].Plaintext, nil
|
||||
}
|
||||
|
||||
// MAC generates the checksum of the given req.Message using the key
|
||||
// with the req.Name at the KMS.
|
||||
func (*kmsConn) MAC(context.Context, *MACRequest) ([]byte, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
312
internal/kms/secret-key.go
Normal file
312
internal/kms/secret-key.go
Normal file
@@ -0,0 +1,312 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/secure-io/sio-go/sioutil"
|
||||
"golang.org/x/crypto/chacha20"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
|
||||
"github.com/minio/kms-go/kms"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/hash/sha256"
|
||||
)
|
||||
|
||||
// ParseSecretKey parses s as <key-id>:<base64> and returns a
|
||||
// KMS that uses s as builtin single key as KMS implementation.
|
||||
func ParseSecretKey(s string) (*KMS, error) {
|
||||
v := strings.SplitN(s, ":", 2)
|
||||
if len(v) != 2 {
|
||||
return nil, errors.New("kms: invalid secret key format")
|
||||
}
|
||||
|
||||
keyID, b64Key := v[0], v[1]
|
||||
key, err := base64.StdEncoding.DecodeString(b64Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewBuiltin(keyID, key)
|
||||
}
|
||||
|
||||
// NewBuiltin returns a single-key KMS that derives new DEKs from the
|
||||
// given key.
|
||||
func NewBuiltin(keyID string, key []byte) (*KMS, error) {
|
||||
if len(key) != 32 {
|
||||
return nil, errors.New("kms: invalid key length " + strconv.Itoa(len(key)))
|
||||
}
|
||||
return &KMS{
|
||||
Type: Builtin,
|
||||
DefaultKey: keyID,
|
||||
conn: secretKey{
|
||||
keyID: keyID,
|
||||
key: key,
|
||||
},
|
||||
latencyBuckets: defaultLatencyBuckets,
|
||||
latency: make([]atomic.Uint64, len(defaultLatencyBuckets)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// secretKey is a KMS implementation that derives new DEKs
|
||||
// from a single key.
|
||||
type secretKey struct {
|
||||
keyID string
|
||||
key []byte
|
||||
}
|
||||
|
||||
// Version returns the version of the builtin KMS.
|
||||
func (secretKey) Version(ctx context.Context) (string, error) { return "v1", nil }
|
||||
|
||||
// APIs returns an error since the builtin KMS does not provide a list of APIs.
|
||||
func (secretKey) APIs(ctx context.Context) ([]madmin.KMSAPI, error) {
|
||||
return nil, ErrNotSupported
|
||||
}
|
||||
|
||||
// Status returns a set of endpoints and their KMS status. Since, the builtin KMS is not
|
||||
// external it returns "127.0.0.1: online".
|
||||
func (secretKey) Status(context.Context) (map[string]madmin.ItemState, error) {
|
||||
return map[string]madmin.ItemState{
|
||||
"127.0.0.1": madmin.ItemOnline,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListKeys returns a list of keys with metadata. The builtin KMS consists of just a single key.
|
||||
func (s secretKey) ListKeys(ctx context.Context, req *ListRequest) ([]madmin.KMSKeyInfo, string, error) {
|
||||
if strings.HasPrefix(s.keyID, req.Prefix) && strings.HasPrefix(s.keyID, req.ContinueAt) {
|
||||
return []madmin.KMSKeyInfo{{Name: s.keyID}}, "", nil
|
||||
}
|
||||
return []madmin.KMSKeyInfo{}, "", nil
|
||||
}
|
||||
|
||||
// CreateKey returns ErrKeyExists unless req.Name is equal to the secretKey name.
|
||||
// The builtin KMS does not support creating multiple keys.
|
||||
func (s secretKey) CreateKey(_ context.Context, req *CreateKeyRequest) error {
|
||||
if req.Name != s.keyID {
|
||||
return ErrNotSupported
|
||||
}
|
||||
return ErrKeyExists
|
||||
}
|
||||
|
||||
// GenerateKey decrypts req.Ciphertext. The key name req.Name must match the key
|
||||
// name of the secretKey.
|
||||
//
|
||||
// The returned DEK is encrypted using AES-GCM and the ciphertext format is compatible
|
||||
// with KES and MinKMS.
|
||||
func (s secretKey) GenerateKey(_ context.Context, req *GenerateKeyRequest) (DEK, error) {
|
||||
if req.Name != s.keyID {
|
||||
return DEK{}, ErrKeyNotFound
|
||||
}
|
||||
associatedData, err := req.AssociatedData.MarshalText()
|
||||
if err != nil {
|
||||
return DEK{}, err
|
||||
}
|
||||
|
||||
const randSize = 28
|
||||
random, err := sioutil.Random(randSize)
|
||||
if err != nil {
|
||||
return DEK{}, err
|
||||
}
|
||||
iv, nonce := random[:16], random[16:]
|
||||
|
||||
prf := hmac.New(sha256.New, s.key)
|
||||
prf.Write(iv)
|
||||
key := prf.Sum(make([]byte, 0, prf.Size()))
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return DEK{}, err
|
||||
}
|
||||
aead, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return DEK{}, err
|
||||
}
|
||||
|
||||
plaintext, err := sioutil.Random(32)
|
||||
if err != nil {
|
||||
return DEK{}, err
|
||||
}
|
||||
ciphertext := aead.Seal(nil, nonce, plaintext, associatedData)
|
||||
ciphertext = append(ciphertext, random...)
|
||||
return DEK{
|
||||
KeyID: req.Name,
|
||||
Version: 0,
|
||||
Plaintext: plaintext,
|
||||
Ciphertext: ciphertext,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts req.Ciphertext. The key name req.Name must match the key
|
||||
// name of the secretKey.
|
||||
//
|
||||
// Decrypt supports decryption of binary-encoded ciphertexts, as produced by KES
|
||||
// and MinKMS, and legacy JSON formatted ciphertexts.
|
||||
func (s secretKey) Decrypt(_ context.Context, req *DecryptRequest) ([]byte, error) {
|
||||
if req.Name != s.keyID {
|
||||
return nil, ErrKeyNotFound
|
||||
}
|
||||
|
||||
const randSize = 28
|
||||
ciphertext, keyType := parseCiphertext(req.Ciphertext)
|
||||
ciphertext, random := ciphertext[:len(ciphertext)-randSize], ciphertext[len(ciphertext)-randSize:]
|
||||
iv, nonce := random[:16], random[16:]
|
||||
|
||||
var aead cipher.AEAD
|
||||
switch keyType {
|
||||
case kms.AES256:
|
||||
mac := hmac.New(sha256.New, s.key)
|
||||
mac.Write(iv)
|
||||
sealingKey := mac.Sum(nil)
|
||||
|
||||
block, err := aes.NewCipher(sealingKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aead, err = cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case kms.ChaCha20:
|
||||
sealingKey, err := chacha20.HChaCha20(s.key, iv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
aead, err = chacha20poly1305.New(sealingKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, ErrDecrypt
|
||||
}
|
||||
|
||||
associatedData, _ := req.AssociatedData.MarshalText()
|
||||
plaintext, err := aead.Open(nil, nonce, ciphertext, associatedData)
|
||||
if err != nil {
|
||||
return nil, ErrDecrypt
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// MAC generate hmac for the request
|
||||
func (s secretKey) MAC(_ context.Context, req *MACRequest) ([]byte, error) {
|
||||
mac := hmac.New(sha256.New, s.key)
|
||||
mac.Write(req.Message)
|
||||
return mac.Sum(make([]byte, 0, mac.Size())), nil
|
||||
}
|
||||
|
||||
// parseCiphertext parses and converts a ciphertext into
|
||||
// the format expected by a secretKey.
|
||||
//
|
||||
// Previous implementations of the secretKey produced a structured
|
||||
// ciphertext. parseCiphertext converts all previously generated
|
||||
// formats into the expected format.
|
||||
func parseCiphertext(b []byte) ([]byte, kms.SecretKeyType) {
|
||||
if len(b) == 0 {
|
||||
return b, kms.AES256
|
||||
}
|
||||
|
||||
if b[0] == '{' && b[len(b)-1] == '}' { // JSON object
|
||||
var c ciphertext
|
||||
if err := c.UnmarshalJSON(b); err != nil {
|
||||
// It may happen that a random ciphertext starts with '{' and ends with '}'.
|
||||
// In such a case, parsing will fail but we must not return an error. Instead
|
||||
// we return the ciphertext as it is.
|
||||
return b, kms.AES256
|
||||
}
|
||||
|
||||
b = b[:0]
|
||||
b = append(b, c.Bytes...)
|
||||
b = append(b, c.IV...)
|
||||
b = append(b, c.Nonce...)
|
||||
return b, c.Algorithm
|
||||
}
|
||||
return b, kms.AES256
|
||||
}
|
||||
|
||||
// ciphertext is a structure that contains the encrypted
|
||||
// bytes and all relevant information to decrypt these
|
||||
// bytes again with a cryptographic key.
|
||||
type ciphertext struct {
|
||||
Algorithm kms.SecretKeyType
|
||||
ID string
|
||||
IV []byte
|
||||
Nonce []byte
|
||||
Bytes []byte
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the given text as JSON-encoded
|
||||
// ciphertext.
|
||||
//
|
||||
// UnmarshalJSON provides backward-compatible unmarsahaling
|
||||
// of existing ciphertext. In the past, ciphertexts were
|
||||
// JSON-encoded. Now, ciphertexts are binary-encoded.
|
||||
// Therefore, there is no MarshalJSON implementation.
|
||||
func (c *ciphertext) UnmarshalJSON(text []byte) error {
|
||||
const (
|
||||
IVSize = 16
|
||||
NonceSize = 12
|
||||
|
||||
AES256GCM = "AES-256-GCM-HMAC-SHA-256"
|
||||
CHACHA20POLY1305 = "ChaCha20Poly1305"
|
||||
)
|
||||
|
||||
type JSON struct {
|
||||
Algorithm string `json:"aead"`
|
||||
ID string `json:"id"`
|
||||
IV []byte `json:"iv"`
|
||||
Nonce []byte `json:"nonce"`
|
||||
Bytes []byte `json:"bytes"`
|
||||
}
|
||||
var value JSON
|
||||
if err := json.Unmarshal(text, &value); err != nil {
|
||||
return ErrDecrypt
|
||||
}
|
||||
|
||||
if value.Algorithm != AES256GCM && value.Algorithm != CHACHA20POLY1305 {
|
||||
return ErrDecrypt
|
||||
}
|
||||
if len(value.IV) != IVSize {
|
||||
return ErrDecrypt
|
||||
}
|
||||
if len(value.Nonce) != NonceSize {
|
||||
return ErrDecrypt
|
||||
}
|
||||
|
||||
switch value.Algorithm {
|
||||
case AES256GCM:
|
||||
c.Algorithm = kms.AES256
|
||||
case CHACHA20POLY1305:
|
||||
c.Algorithm = kms.ChaCha20
|
||||
default:
|
||||
c.Algorithm = 0
|
||||
}
|
||||
c.ID = value.ID
|
||||
c.IV = value.IV
|
||||
c.Nonce = value.Nonce
|
||||
c.Bytes = value.Bytes
|
||||
return nil
|
||||
}
|
||||
90
internal/kms/secret-key_test.go
Normal file
90
internal/kms/secret-key_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSingleKeyRoundtrip(t *testing.T) {
|
||||
KMS, err := ParseSecretKey("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize KMS: %v", err)
|
||||
}
|
||||
|
||||
key, err := KMS.GenerateKey(t.Context(), &GenerateKeyRequest{Name: "my-key"})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate key: %v", err)
|
||||
}
|
||||
plaintext, err := KMS.Decrypt(t.Context(), &DecryptRequest{
|
||||
Name: key.KeyID,
|
||||
Ciphertext: key.Ciphertext,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decrypt key: %v", err)
|
||||
}
|
||||
if !bytes.Equal(key.Plaintext, plaintext) {
|
||||
t.Fatalf("Decrypted key does not match generated one: got %x - want %x", key.Plaintext, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptKey(t *testing.T) {
|
||||
KMS, err := ParseSecretKey("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize KMS: %v", err)
|
||||
}
|
||||
|
||||
for i, test := range decryptKeyTests {
|
||||
dataKey, err := base64.StdEncoding.DecodeString(test.Plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to decode plaintext key: %v", i, err)
|
||||
}
|
||||
plaintext, err := KMS.Decrypt(t.Context(), &DecryptRequest{
|
||||
Name: test.KeyID,
|
||||
Ciphertext: []byte(test.Ciphertext),
|
||||
AssociatedData: test.Context,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: failed to decrypt key: %v", i, err)
|
||||
}
|
||||
if !bytes.Equal(plaintext, dataKey) {
|
||||
t.Fatalf("Test %d: decrypted key does not generated one: got %x - want %x", i, plaintext, dataKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var decryptKeyTests = []struct {
|
||||
KeyID string
|
||||
Plaintext string
|
||||
Ciphertext string
|
||||
Context Context
|
||||
}{
|
||||
{
|
||||
KeyID: "my-key",
|
||||
Plaintext: "zmS7NrG765UZ0ZN85oPjybelxqVvpz01vxsSpOISy2M=",
|
||||
Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"JbI+vwvYww1lCb5VpkAFuQ==","nonce":"ARjIjJxBSD541Gz8","bytes":"KCbEc2sA0TLvA7aWTWa23AdccVfJMpOxwgG8hm+4PaNrxYfy1xFWZg2gEenVrOgv"}`,
|
||||
},
|
||||
{
|
||||
KeyID: "my-key",
|
||||
Plaintext: "UnPWsZgVI+T4L9WGNzFlP1PsP1Z6hn2Fx8ISeZfDGnA=",
|
||||
Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"r4+yfiVbVIYR0Z2I9Fq+6g==","nonce":"2YpwGwE59GcVraI3","bytes":"k/svMglOU7/Kgwv73heG38NWW575XLcFp3SaxQHDMjJGYyRI3Fiygu2OeutGPXNL"}`,
|
||||
Context: Context{"key": "value"},
|
||||
},
|
||||
}
|
||||
125
internal/kms/stub.go
Normal file
125
internal/kms/stub.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/pkg/v3/wildcard"
|
||||
)
|
||||
|
||||
var (
|
||||
// StubCreatedAt is a constant timestamp for testing
|
||||
StubCreatedAt = time.Date(2024, time.January, 1, 15, 0, 0, 0, time.UTC)
|
||||
// StubCreatedBy is a constant created identity for testing
|
||||
StubCreatedBy = "MinIO"
|
||||
)
|
||||
|
||||
// NewStub returns a stub of KMS for testing
|
||||
func NewStub(defaultKeyName string) *KMS {
|
||||
return &KMS{
|
||||
Type: Builtin,
|
||||
DefaultKey: defaultKeyName,
|
||||
latencyBuckets: defaultLatencyBuckets,
|
||||
latency: make([]atomic.Uint64, len(defaultLatencyBuckets)),
|
||||
conn: &StubKMS{
|
||||
KeyNames: []string{defaultKeyName},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// StubKMS is a KMS implementation for tests
|
||||
type StubKMS struct {
|
||||
KeyNames []string
|
||||
}
|
||||
|
||||
// Version returns the type of the KMS.
|
||||
func (s StubKMS) Version(ctx context.Context) (string, error) {
|
||||
return "stub", nil
|
||||
}
|
||||
|
||||
// APIs returns supported APIs
|
||||
func (s StubKMS) APIs(ctx context.Context) ([]madmin.KMSAPI, error) {
|
||||
return []madmin.KMSAPI{
|
||||
{Method: http.MethodGet, Path: "stub/path"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Status returns a set of endpoints and their KMS status.
|
||||
func (s StubKMS) Status(context.Context) (map[string]madmin.ItemState, error) {
|
||||
return map[string]madmin.ItemState{
|
||||
"127.0.0.1": madmin.ItemOnline,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListKeys returns a list of keys with metadata.
|
||||
func (s StubKMS) ListKeys(ctx context.Context, req *ListRequest) ([]madmin.KMSKeyInfo, string, error) {
|
||||
matches := []madmin.KMSKeyInfo{}
|
||||
if req.Prefix == "" {
|
||||
req.Prefix = "*"
|
||||
}
|
||||
for _, keyName := range s.KeyNames {
|
||||
if wildcard.MatchAsPatternPrefix(req.Prefix, keyName) {
|
||||
matches = append(matches, madmin.KMSKeyInfo{Name: keyName, CreatedAt: StubCreatedAt, CreatedBy: StubCreatedBy})
|
||||
}
|
||||
}
|
||||
|
||||
return matches, "", nil
|
||||
}
|
||||
|
||||
// CreateKey creates a new key with the given name.
|
||||
func (s *StubKMS) CreateKey(_ context.Context, req *CreateKeyRequest) error {
|
||||
if s.containsKeyName(req.Name) {
|
||||
return ErrKeyExists
|
||||
}
|
||||
s.KeyNames = append(s.KeyNames, req.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateKey is a non-functional stub.
|
||||
func (s StubKMS) GenerateKey(_ context.Context, req *GenerateKeyRequest) (DEK, error) {
|
||||
if !s.containsKeyName(req.Name) {
|
||||
return DEK{}, ErrKeyNotFound
|
||||
}
|
||||
return DEK{
|
||||
KeyID: req.Name,
|
||||
Version: 0,
|
||||
Plaintext: []byte("stubplaincharswhichare32bytelong"),
|
||||
Ciphertext: []byte("stubplaincharswhichare32bytelong"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Decrypt is a non-functional stub.
|
||||
func (s StubKMS) Decrypt(_ context.Context, req *DecryptRequest) ([]byte, error) {
|
||||
return req.Ciphertext, nil
|
||||
}
|
||||
|
||||
// MAC is a non-functional stub.
|
||||
func (s StubKMS) MAC(_ context.Context, m *MACRequest) ([]byte, error) {
|
||||
return m.Message, nil
|
||||
}
|
||||
|
||||
// containsKeyName returns true if the given key name exists in the stub KMS.
|
||||
func (s *StubKMS) containsKeyName(keyName string) bool {
|
||||
return slices.Contains(s.KeyNames, keyName)
|
||||
}
|
||||
Reference in New Issue
Block a user