kms: add support for MinKMS and remove some unused/broken code (#19368)

This commit adds support for MinKMS. Now, there are three KMS
implementations in `internal/kms`: Builtin, MinIO KES and MinIO KMS.

Adding another KMS integration required some cleanup. In particular:
 - Various KMS APIs that haven't been and are not used have been
   removed. A lot of the code was broken anyway.
 - Metrics are now monitored by the `kms.KMS` itself. For basic
   metrics this is simpler than collecting metrics for external
   servers. In particular, each KES server returns its own metrics
   and no cluster-level view.
 - The builtin KMS now uses the same en/decryption implemented by
   MinKMS and KES. It still supports decryption of the previous
   ciphertext format. It's backwards compatible.
 - Data encryption keys now include a master key version since MinKMS
   supports multiple versions (~4 billion in total and 10000 concurrent)
   per key name.

Signed-off-by: Andreas Auernhammer <github@aead.dev>
This commit is contained in:
Andreas Auernhammer
2024-05-08 01:55:37 +02:00
committed by GitHub
parent 981497799a
commit 8b660e18f2
36 changed files with 1794 additions and 1808 deletions

View File

@@ -17,16 +17,393 @@
package kms
// Top level config constants for KMS
const (
EnvKMSSecretKey = "MINIO_KMS_SECRET_KEY"
EnvKMSSecretKeyFile = "MINIO_KMS_SECRET_KEY_FILE"
EnvKESEndpoint = "MINIO_KMS_KES_ENDPOINT" // One or multiple KES endpoints, separated by ','
EnvKESKeyName = "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
EnvKESClientPassword = "MINIO_KMS_KES_KEY_PASSWORD" // Optional password to decrypt an encrypt TLS private key
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
EnvKESKeyCacheInterval = "MINIO_KMS_KEY_CACHE_INTERVAL" // Period between polls of the KES KMS Master Key cache, to prevent it from being unused and purged
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/minio/kms-go/kes"
"github.com/minio/kms-go/kms"
"github.com/minio/pkg/v2/certs"
"github.com/minio/pkg/v2/ellipses"
"github.com/minio/pkg/v2/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
)
const (
tlsClientSessionCacheSize = 100
)
// 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 := kms.ParseAPIKey(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.Certificates = append(conf.Certificates, cert)
} 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 {
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.Split(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
View 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 {
os.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
View 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
ListKeyNames(context.Context, *ListRequest) ([]string, 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
}

View File

@@ -41,6 +41,13 @@ var dekEncodeDecodeTests = []struct {
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
},
},
{
Key: DEK{
Version: 3,
Plaintext: mustDecodeB64("GM2UvLXp/X8lzqq0mibFC0LayDCGlmTHQhYLj7qAy7Q="),
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
},
},
}
func TestEncodeDecodeDEK(t *testing.T) {

View File

@@ -17,13 +17,112 @@
package kms
// Error encapsulates S3 API error response fields.
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 exit",
}
// 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 {
Err error
APICode string
HTTPStatusCode int
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 {
return e.Err.Error()
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,
}
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) 2015-2022 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"
"github.com/minio/kms-go/kes"
)
// IdentityManager is the generic interface that handles KMS identity operations
type IdentityManager interface {
// DescribeIdentity describes an identity by returning its metadata.
// e.g. which policy is currently assigned and whether its an admin identity.
DescribeIdentity(ctx context.Context, identity string) (*kes.IdentityInfo, error)
// DescribeSelfIdentity describes the identity issuing the request.
// It infers the identity from the TLS client certificate used to authenticate.
// It returns the identity and policy information for the client identity.
DescribeSelfIdentity(ctx context.Context) (*kes.IdentityInfo, *kes.Policy, error)
// ListIdentities lists all identities.
ListIdentities(ctx context.Context) (*kes.ListIter[kes.Identity], error)
}

View File

@@ -18,239 +18,116 @@
package kms
import (
"bytes"
"context"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"net/http"
"sync"
"time"
"github.com/minio/pkg/v2/env"
"github.com/minio/kms-go/kes"
"github.com/minio/pkg/v2/certs"
"github.com/minio/madmin-go/v3"
)
const (
tlsClientSessionCacheSize = 100
)
// Config contains various KMS-related configuration
// parameters - like KMS endpoints or authentication
// credentials.
type Config struct {
// Endpoints contains a list of KMS server
// HTTP endpoints.
Endpoints []string
// DefaultKeyID is the key ID used when
// no explicit key ID is specified for
// a cryptographic operation.
DefaultKeyID string
// APIKey is an credential provided by env. var.
// to authenticate to a KES server. Either an
// API key or a client certificate must be specified.
APIKey kes.APIKey
// Certificate is the client TLS certificate
// to authenticate to KMS via mTLS.
Certificate *certs.Certificate
// ReloadCertEvents is an event channel that receives
// the reloaded client certificate.
ReloadCertEvents <-chan tls.Certificate
// RootCAs is a set of root CA certificates
// to verify the KMS server TLS certificate.
RootCAs *x509.CertPool
}
// NewWithConfig returns a new KMS using the given
// configuration.
func NewWithConfig(config Config, logger Logger) (KMS, error) {
if len(config.Endpoints) == 0 {
return nil, errors.New("kms: no server endpoints")
}
endpoints := make([]string, len(config.Endpoints)) // Copy => avoid being affect by any changes to the original slice
copy(endpoints, config.Endpoints)
var client *kes.Client
if config.APIKey != nil {
cert, err := kes.GenerateCertificate(config.APIKey)
if err != nil {
return nil, err
}
client = kes.NewClientWithConfig("", &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
RootCAs: config.RootCAs,
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
})
} else {
client = kes.NewClientWithConfig("", &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{config.Certificate.Get()},
RootCAs: config.RootCAs,
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
})
}
client.Endpoints = endpoints
c := &kesClient{
client: client,
defaultKeyID: config.DefaultKeyID,
}
go func() {
if config.Certificate == nil || config.ReloadCertEvents == nil {
return
}
var prevCertificate tls.Certificate
for {
certificate, ok := <-config.ReloadCertEvents
if !ok {
return
}
sameCert := len(certificate.Certificate) == len(prevCertificate.Certificate)
for i, b := range certificate.Certificate {
if !sameCert {
break
}
sameCert = sameCert && bytes.Equal(b, prevCertificate.Certificate[i])
}
// Do not reload if its the same cert as before.
if !sameCert {
client := kes.NewClientWithConfig("", &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{certificate},
RootCAs: config.RootCAs,
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
})
client.Endpoints = endpoints
c.lock.Lock()
c.client = client
c.lock.Unlock()
prevCertificate = certificate
}
}
}()
go c.refreshKMSMasterKeyCache(logger)
return c, nil
}
// Request KES keep an up-to-date copy of the KMS master key to allow minio to start up even if KMS is down. The
// cached key may still be evicted if the period of this function is longer than that of KES .cache.expiry.unused
func (c *kesClient) refreshKMSMasterKeyCache(logger Logger) {
ctx := context.Background()
defaultCacheDuration := 10 * time.Second
cacheDuration, err := env.GetDuration(EnvKESKeyCacheInterval, defaultCacheDuration)
if err != nil {
logger.LogOnceIf(ctx, fmt.Errorf("%s, using default of 10s", err.Error()), "refresh-kms-master-key")
cacheDuration = defaultCacheDuration
}
if cacheDuration < time.Second {
logger.LogOnceIf(ctx, errors.New("cache duration is less than 1s, using default of 10s"), "refresh-kms-master-key")
cacheDuration = defaultCacheDuration
}
timer := time.NewTimer(cacheDuration)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return
case <-timer.C:
c.RefreshKey(ctx, logger)
// Reset for the next interval
timer.Reset(cacheDuration)
}
}
}
type kesClient struct {
lock sync.RWMutex
type kesConn struct {
defaultKeyID string
client *kes.Client
}
var ( // compiler checks
_ KMS = (*kesClient)(nil)
_ KeyManager = (*kesClient)(nil)
_ IdentityManager = (*kesClient)(nil)
_ PolicyManager = (*kesClient)(nil)
)
// Stat returns the current KES status containing a
// list of KES endpoints and the default key ID.
func (c *kesClient) Stat(ctx context.Context) (Status, error) {
c.lock.RLock()
defer c.lock.RUnlock()
st, err := c.client.Status(ctx)
if err != nil {
return Status{}, err
}
endpoints := make([]string, len(c.client.Endpoints))
copy(endpoints, c.client.Endpoints)
return Status{
Name: "KES",
Endpoints: endpoints,
DefaultKey: c.defaultKeyID,
Details: st,
}, nil
}
// IsLocal returns true if the KMS is a local implementation
func (c *kesClient) IsLocal() bool {
return env.IsSet(EnvKMSSecretKey)
}
// List returns an array of local KMS Names
func (c *kesClient) List() []kes.KeyInfo {
var kmsSecret []kes.KeyInfo
envKMSSecretKey := env.Get(EnvKMSSecretKey, "")
values := strings.SplitN(envKMSSecretKey, ":", 2)
if len(values) == 2 {
kmsSecret = []kes.KeyInfo{
{
Name: values[0],
},
}
}
return kmsSecret
}
// Metrics retrieves server metrics in the Prometheus exposition format.
func (c *kesClient) Metrics(ctx context.Context) (kes.Metric, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.Metrics(ctx)
}
// Version retrieves version information
func (c *kesClient) Version(ctx context.Context) (string, error) {
c.lock.RLock()
defer c.lock.RUnlock()
func (c *kesConn) Version(ctx context.Context) (string, error) {
return c.client.Version(ctx)
}
// APIs retrieves a list of supported API endpoints
func (c *kesClient) APIs(ctx context.Context) ([]kes.API, error) {
c.lock.RLock()
defer c.lock.RUnlock()
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,
}
}
return c.client.APIs(ctx)
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) ListKeyNames(ctx context.Context, req *ListRequest) ([]string, string, error) {
return c.client.ListKeys(ctx, req.Prefix, req.Limit)
}
// CreateKey tries to create a new key at the KMS with the
@@ -258,32 +135,34 @@ func (c *kesClient) APIs(ctx context.Context) ([]kes.API, error) {
//
// If the a key with the same keyID already exists then
// CreateKey returns kes.ErrKeyExists.
func (c *kesClient) CreateKey(ctx context.Context, keyID string) error {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.CreateKey(ctx, keyID)
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 *kesClient) DeleteKey(ctx context.Context, keyID string) error {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DeleteKey(ctx, keyID)
}
// ListKeys returns an iterator over all key names.
func (c *kesClient) ListKeys(ctx context.Context) (*kes.ListIter[string], error) {
c.lock.RLock()
defer c.lock.RUnlock()
return &kes.ListIter[string]{
NextFunc: c.client.ListKeys,
}, nil
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
@@ -294,34 +173,36 @@ func (c *kesClient) ListKeys(ctx context.Context) (*kes.ListIter[string], error)
// 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 *kesClient) GenerateKey(ctx context.Context, keyID string, cryptoCtx Context) (DEK, error) {
c.lock.RLock()
defer c.lock.RUnlock()
if keyID == "" {
keyID = c.defaultKeyID
}
ctxBytes, err := cryptoCtx.MarshalText()
func (c *kesConn) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) {
aad, err := req.AssociatedData.MarshalText()
if err != nil {
return DEK{}, err
}
dek, err := c.client.GenerateKey(ctx, keyID, ctxBytes)
name := req.Name
if name == "" {
name = c.defaultKeyID
}
dek, err := c.client.GenerateKey(ctx, name, aad)
if err != nil {
return DEK{}, err
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: keyID,
KeyID: name,
Plaintext: dek.Plaintext,
Ciphertext: dek.Ciphertext,
}, nil
}
// ImportKey imports a cryptographic key into the KMS.
func (c *kesClient) ImportKey(ctx context.Context, keyID string, bytes []byte) error {
c.lock.RLock()
defer c.lock.RUnlock()
func (c *kesConn) ImportKey(ctx context.Context, keyID string, bytes []byte) error {
return c.client.ImportKey(ctx, keyID, &kes.ImportKeyRequest{
Key: bytes,
})
@@ -329,10 +210,7 @@ func (c *kesClient) ImportKey(ctx context.Context, keyID string, bytes []byte) e
// EncryptKey Encrypts and authenticates a (small) plaintext with the cryptographic key
// The plaintext must not exceed 1 MB
func (c *kesClient) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
func (c *kesConn) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) {
ctxBytes, err := ctx.MarshalText()
if err != nil {
return nil, err
@@ -343,184 +221,42 @@ func (c *kesClient) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]b
// 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 *kesClient) DecryptKey(keyID string, ciphertext []byte, ctx Context) ([]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
ctxBytes, err := ctx.MarshalText()
func (c *kesConn) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) {
aad, err := req.AssociatedData.MarshalText()
if err != nil {
return nil, err
}
return c.client.Decrypt(context.Background(), keyID, ciphertext, ctxBytes)
}
func (c *kesClient) DecryptAll(ctx context.Context, keyID string, ciphertexts [][]byte, contexts []Context) ([][]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
plaintexts := make([][]byte, 0, len(ciphertexts))
for i := range ciphertexts {
ctxBytes, err := contexts[i].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
}
plaintext, err := c.client.Decrypt(ctx, keyID, ciphertexts[i], ctxBytes)
if err != nil {
return nil, err
if errors.Is(err, kes.ErrDecrypt) {
return nil, ErrDecrypt
}
plaintexts = append(plaintexts, plaintext)
if errors.Is(err, kes.ErrNotAllowed) {
return nil, ErrPermission
}
return nil, errDecryptionFailed(err)
}
return plaintexts, nil
return plaintext, nil
}
// HMAC generates the HMAC checksum of the given msg using the key
// with the given keyID at the KMS.
func (c *kesClient) HMAC(ctx context.Context, keyID string, msg []byte) ([]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.HMAC(context.Background(), keyID, msg)
}
// DescribePolicy describes a policy by returning its metadata.
// e.g. who created the policy at which point in time.
func (c *kesClient) DescribePolicy(ctx context.Context, policy string) (*kes.PolicyInfo, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DescribePolicy(ctx, policy)
}
// ListPolicies returns an iterator over all policy names.
func (c *kesClient) ListPolicies(ctx context.Context) (*kes.ListIter[string], error) {
c.lock.RLock()
defer c.lock.RUnlock()
return &kes.ListIter[string]{
NextFunc: c.client.ListPolicies,
}, nil
}
// GetPolicy gets a policy from KMS.
func (c *kesClient) GetPolicy(ctx context.Context, policy string) (*kes.Policy, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.GetPolicy(ctx, policy)
}
// DescribeIdentity describes an identity by returning its metadata.
// e.g. which policy is currently assigned and whether its an admin identity.
func (c *kesClient) DescribeIdentity(ctx context.Context, identity string) (*kes.IdentityInfo, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DescribeIdentity(ctx, kes.Identity(identity))
}
// DescribeSelfIdentity describes the identity issuing the request.
// It infers the identity from the TLS client certificate used to authenticate.
// It returns the identity and policy information for the client identity.
func (c *kesClient) DescribeSelfIdentity(ctx context.Context) (*kes.IdentityInfo, *kes.Policy, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DescribeSelf(ctx)
}
// ListIdentities returns an iterator over all identities.
func (c *kesClient) ListIdentities(ctx context.Context) (*kes.ListIter[kes.Identity], error) {
c.lock.RLock()
defer c.lock.RUnlock()
return &kes.ListIter[kes.Identity]{
NextFunc: c.client.ListIdentities,
}, nil
}
// Verify verifies all KMS endpoints and returns details
func (c *kesClient) Verify(ctx context.Context) []VerifyResult {
c.lock.RLock()
defer c.lock.RUnlock()
results := []VerifyResult{}
kmsContext := Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
for _, endpoint := range c.client.Endpoints {
client := kes.Client{
Endpoints: []string{endpoint},
HTTPClient: c.client.HTTPClient,
// 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
}
// 1. Get stats for the KES instance
state, err := client.Status(ctx)
if err != nil {
results = append(results, VerifyResult{Status: "offline", Endpoint: endpoint})
continue
if errors.Is(err, kes.ErrNotAllowed) {
return nil, ErrPermission
}
// 2. Generate a new key using the KMS.
kmsCtx, err := kmsContext.MarshalText()
if err != nil {
results = append(results, VerifyResult{Status: "offline", Endpoint: endpoint})
continue
}
result := VerifyResult{Status: "online", Endpoint: endpoint, Version: state.Version}
key, err := client.GenerateKey(ctx, env.Get(EnvKESKeyName, ""), kmsCtx)
if err != nil {
result.Encrypt = fmt.Sprintf("Encryption failed: %v", err)
} else {
result.Encrypt = "success"
}
// 3. Verify that we can indeed decrypt the (encrypted) key
decryptedKey, err := client.Decrypt(ctx, env.Get(EnvKESKeyName, ""), key.Ciphertext, kmsCtx)
switch {
case err != nil:
result.Decrypt = fmt.Sprintf("Decryption failed: %v", err)
case subtle.ConstantTimeCompare(key.Plaintext, decryptedKey) != 1:
result.Decrypt = "Decryption failed: decrypted key does not match generated key"
default:
result.Decrypt = "success"
}
results = append(results, result)
}
return results
}
// Logger interface permits access to module specific logging, in this case, for KMS
type Logger interface {
LogOnceIf(ctx context.Context, err error, id string, errKind ...interface{})
LogIf(ctx context.Context, err error, errKind ...interface{})
}
// RefreshKey checks the validity of the KMS Master Key
func (c *kesClient) RefreshKey(ctx context.Context, logger Logger) bool {
c.lock.RLock()
defer c.lock.RUnlock()
validKey := false
kmsContext := Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
for _, endpoint := range c.client.Endpoints {
client := kes.Client{
Endpoints: []string{endpoint},
HTTPClient: c.client.HTTPClient,
}
// 1. Generate a new key using the KMS.
kmsCtx, err := kmsContext.MarshalText()
if err != nil {
logger.LogOnceIf(ctx, err, "refresh-kms-master-key")
validKey = false
break
}
_, err = client.GenerateKey(ctx, env.Get(EnvKESKeyName, ""), kmsCtx)
if err != nil {
logger.LogOnceIf(ctx, err, "refresh-kms-master-key")
validKey = false
break
}
if !validKey {
validKey = true
if kErr, ok := err.(kes.Error); ok && kErr.Status() == http.StatusNotImplemented {
return nil, ErrNotSupported
}
}
return validKey
return mac, nil
}

View File

@@ -1,50 +0,0 @@
// Copyright (c) 2015-2022 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"
"github.com/minio/kms-go/kes"
)
// KeyManager is the generic interface that handles KMS key operations
type KeyManager interface {
// CreateKey creates a new key at the KMS with the given key ID.
CreateKey(ctx context.Context, keyID string) error
// 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.
DeleteKey(ctx context.Context, keyID string) error
// ListKeys lists all key names.
ListKeys(ctx context.Context) (*kes.ListIter[string], error)
// ImportKey imports a cryptographic key into the KMS.
ImportKey(ctx context.Context, keyID string, bytes []byte) error
// EncryptKey Encrypts and authenticates a (small) plaintext with the cryptographic key
// The plaintext must not exceed 1 MB
EncryptKey(keyID string, plaintext []byte, context Context) ([]byte, error)
// HMAC computes the HMAC of the given msg and key with the given
// key ID.
HMAC(ctx context.Context, keyID string, msg []byte) ([]byte, error)
}

View File

@@ -19,132 +19,403 @@ package kms
import (
"context"
"encoding"
"encoding/json"
"errors"
"net/http"
"slices"
"sync/atomic"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/minio/kms-go/kes"
"github.com/minio/kms-go/kms"
"github.com/minio/madmin-go/v3"
)
// KMS is the generic interface that abstracts over
// different KMS implementations.
type KMS interface {
// Stat returns the current KMS status.
Stat(cxt context.Context) (Status, error)
// 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
// IsLocal returns true if the KMS is a local implementation
IsLocal() bool
// ContinueAt is the name of the element from where
// a listing should continue. It allows paginated
// listings.
ContinueAt string
// List returns an array of local KMS Names
List() []kes.KeyInfo
// Metrics returns a KMS metric snapshot.
Metrics(ctx context.Context) (kes.Metric, error)
// CreateKey creates a new key at the KMS with the given key ID.
CreateKey(ctx context.Context, keyID 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(ctx context.Context, keyID string, context Context) (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.
DecryptKey(keyID string, ciphertext []byte, context Context) ([]byte, error)
// DecryptAll decrypts all ciphertexts with the key referenced
// by the key ID. The contexts must match the context value
// used to generate the ciphertexts.
DecryptAll(ctx context.Context, keyID string, ciphertext [][]byte, context []Context) ([][]byte, error)
// Verify verifies all KMS endpoints and returns the details
Verify(cxt context.Context) []VerifyResult
// Limit limits the number of elements returned by
// a single list operation. If <= 0, a reasonable
// limit is selected automatically.
Limit int
}
// VerifyResult describes the verification result details a KMS endpoint
type VerifyResult struct {
Endpoint string
Decrypt string
Encrypt string
Version string
Status string
// 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
}
// Status describes the current state of a KMS.
type Status struct {
Name string // The name of the KMS
Endpoints []string // A set of the KMS endpoints
// 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
}
// DefaultKey is the key used when no explicit key ID
// is specified. It is empty if the KMS does not support
// a default key.
// 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
// Details provides more details about the KMS endpoint status.
// including uptime, version and available CPUs.
// Could be more in future.
Details kes.State
conn conn // Connection to the KMS
// Metrics
reqOK, reqErr, reqFail atomic.Uint64
latencyBuckets []time.Duration // expected to be sorted
latency []atomic.Uint64
}
// DEK is a data encryption key. It consists of a
// plaintext-ciphertext pair and the ID of the key
// used to generate the ciphertext.
// Version returns version information about the KMS.
//
// 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
Plaintext []byte
Ciphertext []byte
// 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)
}
var (
_ encoding.TextMarshaler = (*DEK)(nil)
_ encoding.TextUnmarshaler = (*DEK)(nil)
)
// 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)
}
// MarshalText encodes the DEK's key ID and ciphertext
// as JSON.
func (d DEK) MarshalText() ([]byte, error) {
type JSON struct {
KeyID string `json:"keyid"`
Ciphertext []byte `json:"ciphertext"`
// 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 json.Marshal(JSON{
KeyID: d.KeyID,
Ciphertext: d.Ciphertext,
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
}
// ListKeyNames returns a list of key names and a potential
// next name from where to continue a subsequent listing.
func (k *KMS) ListKeyNames(ctx context.Context, req *ListRequest) ([]string, string, error) {
if req.Prefix == "*" {
req.Prefix = ""
}
return k.conn.ListKeyNames(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) ListKeyNames(ctx context.Context, req *ListRequest) ([]string, 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)
}
names := make([]string, 0, len(resp.Items))
for _, item := range resp.Items {
names = append(names, item.Name)
}
return names, resp.ContinueAt, nil
}
// 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"`
Ciphertext []byte `json:"ciphertext"`
func (c *kmsConn) CreateKey(ctx context.Context, req *CreateKeyRequest) error {
if err := c.client.CreateKey(ctx, &kms.CreateKeyRequest{
Enclave: c.enclave,
Name: req.Name,
}); err != nil {
if errors.Is(err, kms.ErrKeyExists) {
return ErrKeyExists
}
if errors.Is(err, kms.ErrPermission) {
return ErrPermission
}
return errKeyCreationFailed(err)
}
var v JSON
json := jsoniter.ConfigCompatibleWithStandardLibrary
if err := json.Unmarshal(text, &v); err != nil {
return err
}
d.KeyID, d.Plaintext, d.Ciphertext = v.KeyID, nil, v.Ciphertext
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, &kms.GenerateKeyRequest{
Enclave: c.enclave,
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.Version,
Plaintext: resp.Plaintext,
Ciphertext: resp.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, &kms.DecryptRequest{
Enclave: c.enclave,
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.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
}

View File

@@ -1,37 +0,0 @@
// Copyright (c) 2015-2022 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"
"github.com/minio/kms-go/kes"
)
// PolicyManager is the generic interface that handles KMS policy] operations
type PolicyManager interface {
// DescribePolicy describes a policy by returning its metadata.
// e.g. who created the policy at which point in time.
DescribePolicy(ctx context.Context, policy string) (*kes.PolicyInfo, error)
// GetPolicy gets a policy from KMS.
GetPolicy(ctx context.Context, policy string) (*kes.Policy, error)
// ListPolicies lists all policies.
ListPolicies(ctx context.Context) (*kes.ListIter[string], error)
}

309
internal/kms/secret-key.go Normal file
View File

@@ -0,0 +1,309 @@
// 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
}
// ListKeyNames returns a list of key names. The builtin KMS consists of just a single key.
func (s secretKey) ListKeyNames(ctx context.Context, req *ListRequest) ([]string, string, error) {
if strings.HasPrefix(s.keyID, req.Prefix) && strings.HasPrefix(s.keyID, req.ContinueAt) {
return []string{s.keyID}, "", nil
}
return []string{}, "", 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
}
func (secretKey) MAC(context.Context, *MACRequest) ([]byte, error) {
return nil, ErrNotSupported
}
// 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
}

View File

@@ -25,16 +25,19 @@ import (
)
func TestSingleKeyRoundtrip(t *testing.T) {
KMS, err := Parse("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
KMS, err := ParseSecretKey("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
if err != nil {
t.Fatalf("Failed to initialize KMS: %v", err)
}
key, err := KMS.GenerateKey(context.Background(), "my-key", Context{})
key, err := KMS.GenerateKey(context.Background(), &GenerateKeyRequest{Name: "my-key"})
if err != nil {
t.Fatalf("Failed to generate key: %v", err)
}
plaintext, err := KMS.DecryptKey(key.KeyID, key.Ciphertext, Context{})
plaintext, err := KMS.Decrypt(context.TODO(), &DecryptRequest{
Name: key.KeyID,
Ciphertext: key.Ciphertext,
})
if err != nil {
t.Fatalf("Failed to decrypt key: %v", err)
}
@@ -44,7 +47,7 @@ func TestSingleKeyRoundtrip(t *testing.T) {
}
func TestDecryptKey(t *testing.T) {
KMS, err := Parse("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
KMS, err := ParseSecretKey("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
if err != nil {
t.Fatalf("Failed to initialize KMS: %v", err)
}
@@ -54,11 +57,11 @@ func TestDecryptKey(t *testing.T) {
if err != nil {
t.Fatalf("Test %d: failed to decode plaintext key: %v", i, err)
}
ciphertext, err := base64.StdEncoding.DecodeString(test.Ciphertext)
if err != nil {
t.Fatalf("Test %d: failed to decode ciphertext key: %v", i, err)
}
plaintext, err := KMS.DecryptKey(test.KeyID, ciphertext, test.Context)
plaintext, err := KMS.Decrypt(context.TODO(), &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)
}
@@ -77,12 +80,12 @@ var decryptKeyTests = []struct {
{
KeyID: "my-key",
Plaintext: "zmS7NrG765UZ0ZN85oPjybelxqVvpz01vxsSpOISy2M=",
Ciphertext: "eyJhZWFkIjoiQ2hhQ2hhMjBQb2x5MTMwNSIsIml2IjoiSmJJK3Z3dll3dzFsQ2I1VnBrQUZ1UT09Iiwibm9uY2UiOiJBUmpJakp4QlNENTQxR3o4IiwiYnl0ZXMiOiJLQ2JFYzJzQTBUTHZBN2FXVFdhMjNBZGNjVmZKTXBPeHdnRzhobSs0UGFOcnhZZnkxeEZXWmcyZ0VlblZyT2d2In0=",
Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"JbI+vwvYww1lCb5VpkAFuQ==","nonce":"ARjIjJxBSD541Gz8","bytes":"KCbEc2sA0TLvA7aWTWa23AdccVfJMpOxwgG8hm+4PaNrxYfy1xFWZg2gEenVrOgv"}`,
},
{
KeyID: "my-key",
Plaintext: "UnPWsZgVI+T4L9WGNzFlP1PsP1Z6hn2Fx8ISeZfDGnA=",
Ciphertext: "eyJhZWFkIjoiQ2hhQ2hhMjBQb2x5MTMwNSIsIml2IjoicjQreWZpVmJWSVlSMFoySTlGcSs2Zz09Iiwibm9uY2UiOiIyWXB3R3dFNTlHY1ZyYUkzIiwiYnl0ZXMiOiJrL3N2TWdsT1U3L0tnd3Y3M2hlRzM4TldXNTc1WExjRnAzU2F4UUhETWpKR1l5UkkzRml5Z3UyT2V1dEdQWE5MIn0=",
Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"r4+yfiVbVIYR0Z2I9Fq+6g==","nonce":"2YpwGwE59GcVraI3","bytes":"k/svMglOU7/Kgwv73heG38NWW575XLcFp3SaxQHDMjJGYyRI3Fiygu2OeutGPXNL"}`,
Context: Context{"key": "value"},
},
}

View File

@@ -1,318 +0,0 @@
// 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"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
jsoniter "github.com/json-iterator/go"
"github.com/secure-io/sio-go/sioutil"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"github.com/minio/kms-go/kes"
"github.com/minio/minio/internal/hash/sha256"
)
// Parse parses s as single-key KMS. The given string
// is expected to have the following format:
//
// <key-id>:<base64-key>
//
// The returned KMS implementation uses the parsed
// key ID and key to derive new DEKs and decrypt ciphertext.
func Parse(s string) (KMS, error) {
v := strings.SplitN(s, ":", 2)
if len(v) != 2 {
return nil, errors.New("kms: invalid master key format")
}
keyID, b64Key := v[0], v[1]
key, err := base64.StdEncoding.DecodeString(b64Key)
if err != nil {
return nil, err
}
return New(keyID, key)
}
// New returns a single-key KMS that derives new DEKs from the
// given key.
func New(keyID string, key []byte) (KMS, error) {
if len(key) != 32 {
return nil, errors.New("kms: invalid key length " + strconv.Itoa(len(key)))
}
return secretKey{
keyID: keyID,
key: key,
}, nil
}
// secretKey is a KMS implementation that derives new DEKs
// from a single key.
type secretKey struct {
keyID string
key []byte
}
var _ KMS = secretKey{} // compiler check
const ( // algorithms used to derive and encrypt DEKs
algorithmAESGCM = "AES-256-GCM-HMAC-SHA-256"
algorithmChaCha20Poly1305 = "ChaCha20Poly1305"
)
func (kms secretKey) Stat(context.Context) (Status, error) {
return Status{
Name: "SecretKey",
DefaultKey: kms.keyID,
}, nil
}
// IsLocal returns true if the KMS is a local implementation
func (kms secretKey) IsLocal() bool {
return true
}
// List returns an array of local KMS Names
func (kms secretKey) List() []kes.KeyInfo {
kmsSecret := []kes.KeyInfo{
{
Name: kms.keyID,
},
}
return kmsSecret
}
func (secretKey) Metrics(ctx context.Context) (kes.Metric, error) {
return kes.Metric{}, Error{
HTTPStatusCode: http.StatusNotImplemented,
APICode: "KMS.NotImplemented",
Err: errors.New("metrics are not supported"),
}
}
func (kms secretKey) CreateKey(_ context.Context, keyID string) error {
if keyID == kms.keyID {
return nil
}
return Error{
HTTPStatusCode: http.StatusNotImplemented,
APICode: "KMS.NotImplemented",
Err: fmt.Errorf("creating custom key %q is not supported", keyID),
}
}
func (kms secretKey) GenerateKey(_ context.Context, keyID string, context Context) (DEK, error) {
if keyID == "" {
keyID = kms.keyID
}
if keyID != kms.keyID {
return DEK{}, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.NotFoundException",
Err: fmt.Errorf("key %q does not exist", keyID),
}
}
iv, err := sioutil.Random(16)
if err != nil {
return DEK{}, err
}
var algorithm string
if sioutil.NativeAES() {
algorithm = algorithmAESGCM
} else {
algorithm = algorithmChaCha20Poly1305
}
var aead cipher.AEAD
switch algorithm {
case algorithmAESGCM:
mac := hmac.New(sha256.New, kms.key)
mac.Write(iv)
sealingKey := mac.Sum(nil)
var block cipher.Block
block, err = aes.NewCipher(sealingKey)
if err != nil {
return DEK{}, err
}
aead, err = cipher.NewGCM(block)
if err != nil {
return DEK{}, err
}
case algorithmChaCha20Poly1305:
var sealingKey []byte
sealingKey, err = chacha20.HChaCha20(kms.key, iv)
if err != nil {
return DEK{}, err
}
aead, err = chacha20poly1305.New(sealingKey)
if err != nil {
return DEK{}, err
}
default:
return DEK{}, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.InternalException",
Err: errors.New("invalid algorithm: " + algorithm),
}
}
nonce, err := sioutil.Random(aead.NonceSize())
if err != nil {
return DEK{}, err
}
plaintext, err := sioutil.Random(32)
if err != nil {
return DEK{}, err
}
associatedData, _ := context.MarshalText()
ciphertext := aead.Seal(nil, nonce, plaintext, associatedData)
json := jsoniter.ConfigCompatibleWithStandardLibrary
ciphertext, err = json.Marshal(encryptedKey{
Algorithm: algorithm,
IV: iv,
Nonce: nonce,
Bytes: ciphertext,
})
if err != nil {
return DEK{}, err
}
return DEK{
KeyID: keyID,
Plaintext: plaintext,
Ciphertext: ciphertext,
}, nil
}
func (kms secretKey) DecryptKey(keyID string, ciphertext []byte, context Context) ([]byte, error) {
if keyID != kms.keyID {
return nil, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.NotFoundException",
Err: fmt.Errorf("key %q does not exist", keyID),
}
}
var encryptedKey encryptedKey
json := jsoniter.ConfigCompatibleWithStandardLibrary
if err := json.Unmarshal(ciphertext, &encryptedKey); err != nil {
return nil, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.InternalException",
Err: err,
}
}
if n := len(encryptedKey.IV); n != 16 {
return nil, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.InternalException",
Err: fmt.Errorf("invalid iv size: %d", n),
}
}
var aead cipher.AEAD
switch encryptedKey.Algorithm {
case algorithmAESGCM:
mac := hmac.New(sha256.New, kms.key)
mac.Write(encryptedKey.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 algorithmChaCha20Poly1305:
sealingKey, err := chacha20.HChaCha20(kms.key, encryptedKey.IV)
if err != nil {
return nil, err
}
aead, err = chacha20poly1305.New(sealingKey)
if err != nil {
return nil, err
}
default:
return nil, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.InternalException",
Err: fmt.Errorf("invalid algorithm: %q", encryptedKey.Algorithm),
}
}
if n := len(encryptedKey.Nonce); n != aead.NonceSize() {
return nil, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.InternalException",
Err: fmt.Errorf("invalid nonce size %d", n),
}
}
associatedData, _ := context.MarshalText()
plaintext, err := aead.Open(nil, encryptedKey.Nonce, encryptedKey.Bytes, associatedData)
if err != nil {
return nil, Error{
HTTPStatusCode: http.StatusBadRequest,
APICode: "KMS.InternalException",
Err: fmt.Errorf("encrypted key is not authentic"),
}
}
return plaintext, nil
}
func (kms secretKey) DecryptAll(_ context.Context, keyID string, ciphertexts [][]byte, contexts []Context) ([][]byte, error) {
plaintexts := make([][]byte, 0, len(ciphertexts))
for i := range ciphertexts {
plaintext, err := kms.DecryptKey(keyID, ciphertexts[i], contexts[i])
if err != nil {
return nil, err
}
plaintexts = append(plaintexts, plaintext)
}
return plaintexts, nil
}
// Verify verifies all KMS endpoints and returns details
func (kms secretKey) Verify(cxt context.Context) []VerifyResult {
return []VerifyResult{
{Endpoint: "self"},
}
}
type encryptedKey struct {
Algorithm string `json:"aead"`
IV []byte `json:"iv"`
Nonce []byte `json:"nonce"`
Bytes []byte `json:"bytes"`
}

View File

@@ -1,32 +0,0 @@
// Copyright (c) 2015-2022 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"
"github.com/minio/kms-go/kes"
)
// StatusManager is the generic interface that handles KMS status operations
type StatusManager interface {
// Version retrieves version information
Version(ctx context.Context) (string, error)
// APIs retrieves a list of supported API endpoints
APIs(ctx context.Context) ([]kes.API, error)
}