mirror of
https://github.com/minio/minio.git
synced 2024-12-25 06:35:56 -05:00
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:
parent
981497799a
commit
8b660e18f2
@ -874,8 +874,10 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r *
|
|||||||
}
|
}
|
||||||
kmsKey := encConfig.KeyID()
|
kmsKey := encConfig.KeyID()
|
||||||
if kmsKey != "" {
|
if kmsKey != "" {
|
||||||
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
_, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
|
||||||
_, err := GlobalKMS.GenerateKey(ctx, kmsKey, kmsContext)
|
Name: kmsKey,
|
||||||
|
AssociatedData: kms.Context{"MinIO admin API": "ServerInfoHandler"}, // Context for a test key operation
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
rpt.SetStatus(bucket, fileName, errKMSKeyNotFound)
|
rpt.SetStatus(bucket, fileName, errKMSKeyNotFound)
|
||||||
|
@ -2173,7 +2173,9 @@ func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := GlobalKMS.CreateKey(ctx, r.Form.Get("key-id")); err != nil {
|
if err := GlobalKMS.CreateKey(ctx, &kms.CreateKeyRequest{
|
||||||
|
Name: r.Form.Get("key-id"),
|
||||||
|
}); err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -2194,22 +2196,12 @@ func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, err := GlobalKMS.Stat(ctx)
|
stat, err := GlobalKMS.Status(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
resp, err := json.Marshal(stat)
|
||||||
status := madmin.KMSStatus{
|
|
||||||
Name: stat.Name,
|
|
||||||
DefaultKeyID: stat.DefaultKey,
|
|
||||||
Endpoints: make(map[string]madmin.ItemState, len(stat.Endpoints)),
|
|
||||||
}
|
|
||||||
for _, endpoint := range stat.Endpoints {
|
|
||||||
status.Endpoints[endpoint] = madmin.ItemOnline // TODO(aead): Implement an online check for mTLS
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := json.Marshal(status)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
@ -2231,15 +2223,9 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, err := GlobalKMS.Stat(ctx)
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
keyID := r.Form.Get("key-id")
|
keyID := r.Form.Get("key-id")
|
||||||
if keyID == "" {
|
if keyID == "" {
|
||||||
keyID = stat.DefaultKey
|
keyID = GlobalKMS.DefaultKey
|
||||||
}
|
}
|
||||||
response := madmin.KMSKeyStatus{
|
response := madmin.KMSKeyStatus{
|
||||||
KeyID: keyID,
|
KeyID: keyID,
|
||||||
@ -2247,7 +2233,10 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
||||||
// 1. Generate a new key using the KMS.
|
// 1. Generate a new key using the KMS.
|
||||||
key, err := GlobalKMS.GenerateKey(ctx, keyID, kmsContext)
|
key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
|
||||||
|
Name: keyID,
|
||||||
|
AssociatedData: kmsContext,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.EncryptionErr = err.Error()
|
response.EncryptionErr = err.Error()
|
||||||
resp, err := json.Marshal(response)
|
resp, err := json.Marshal(response)
|
||||||
@ -2260,7 +2249,11 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Verify that we can indeed decrypt the (encrypted) key
|
// 2. Verify that we can indeed decrypt the (encrypted) key
|
||||||
decryptedKey, err := GlobalKMS.DecryptKey(key.KeyID, key.Ciphertext, kmsContext)
|
decryptedKey, err := GlobalKMS.Decrypt(ctx, &kms.DecryptRequest{
|
||||||
|
Name: key.KeyID,
|
||||||
|
Ciphertext: key.Ciphertext,
|
||||||
|
AssociatedData: kmsContext,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.DecryptionErr = err.Error()
|
response.DecryptionErr = err.Error()
|
||||||
resp, err := json.Marshal(response)
|
resp, err := json.Marshal(response)
|
||||||
@ -2413,8 +2406,7 @@ func getServerInfo(ctx context.Context, pools, metrics bool, r *http.Request) ma
|
|||||||
|
|
||||||
domain := globalDomainNames
|
domain := globalDomainNames
|
||||||
services := madmin.Services{
|
services := madmin.Services{
|
||||||
KMS: fetchKMSStatus(),
|
KMSStatus: fetchKMSStatus(ctx),
|
||||||
KMSStatus: fetchKMSStatusV2(ctx),
|
|
||||||
LDAP: ldap,
|
LDAP: ldap,
|
||||||
Logger: log,
|
Logger: log,
|
||||||
Audit: audit,
|
Audit: audit,
|
||||||
@ -3024,66 +3016,25 @@ func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus {
|
|||||||
return notify
|
return notify
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchKMSStatus fetches KMS-related status information.
|
// fetchKMSStatus fetches KMS-related status information for all instances
|
||||||
func fetchKMSStatus() madmin.KMS {
|
func fetchKMSStatus(ctx context.Context) []madmin.KMS {
|
||||||
kmsStat := madmin.KMS{}
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
kmsStat.Status = "disabled"
|
|
||||||
return kmsStat
|
|
||||||
}
|
|
||||||
|
|
||||||
stat, err := GlobalKMS.Stat(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
kmsStat.Status = string(madmin.ItemOffline)
|
|
||||||
return kmsStat
|
|
||||||
}
|
|
||||||
if len(stat.Endpoints) == 0 {
|
|
||||||
kmsStat.Status = stat.Name
|
|
||||||
return kmsStat
|
|
||||||
}
|
|
||||||
kmsStat.Status = string(madmin.ItemOnline)
|
|
||||||
|
|
||||||
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
|
||||||
// 1. Generate a new key using the KMS.
|
|
||||||
key, err := GlobalKMS.GenerateKey(context.Background(), "", kmsContext)
|
|
||||||
if err != nil {
|
|
||||||
kmsStat.Encrypt = fmt.Sprintf("Encryption failed: %v", err)
|
|
||||||
} else {
|
|
||||||
kmsStat.Encrypt = "success"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Verify that we can indeed decrypt the (encrypted) key
|
|
||||||
decryptedKey, err := GlobalKMS.DecryptKey(key.KeyID, key.Ciphertext, kmsContext)
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
kmsStat.Decrypt = fmt.Sprintf("Decryption failed: %v", err)
|
|
||||||
case subtle.ConstantTimeCompare(key.Plaintext, decryptedKey) != 1:
|
|
||||||
kmsStat.Decrypt = "Decryption failed: decrypted key does not match generated key"
|
|
||||||
default:
|
|
||||||
kmsStat.Decrypt = "success"
|
|
||||||
}
|
|
||||||
return kmsStat
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetchKMSStatusV2 fetches KMS-related status information for all instances
|
|
||||||
func fetchKMSStatusV2(ctx context.Context) []madmin.KMS {
|
|
||||||
if GlobalKMS == nil {
|
if GlobalKMS == nil {
|
||||||
return []madmin.KMS{}
|
return []madmin.KMS{}
|
||||||
}
|
}
|
||||||
|
|
||||||
results := GlobalKMS.Verify(ctx)
|
stat, err := GlobalKMS.Status(ctx)
|
||||||
|
if err != nil {
|
||||||
stats := []madmin.KMS{}
|
kmsLogIf(ctx, err, "failed to fetch KMS status information")
|
||||||
for _, result := range results {
|
return []madmin.KMS{}
|
||||||
stats = append(stats, madmin.KMS{
|
|
||||||
Status: result.Status,
|
|
||||||
Endpoint: result.Endpoint,
|
|
||||||
Encrypt: result.Encrypt,
|
|
||||||
Decrypt: result.Decrypt,
|
|
||||||
Version: result.Version,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := make([]madmin.KMS, 0, len(stat.Endpoints))
|
||||||
|
for endpoint, state := range stat.Endpoints {
|
||||||
|
stats = append(stats, madmin.KMS{
|
||||||
|
Status: string(state),
|
||||||
|
Endpoint: endpoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2430,9 +2430,9 @@ func toAPIError(ctx context.Context, err error) APIError {
|
|||||||
switch e := err.(type) {
|
switch e := err.(type) {
|
||||||
case kms.Error:
|
case kms.Error:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
Description: e.Err.Error(),
|
|
||||||
Code: e.APICode,
|
Code: e.APICode,
|
||||||
HTTPStatusCode: e.HTTPStatusCode,
|
Description: e.Err,
|
||||||
|
HTTPStatusCode: e.Code,
|
||||||
}
|
}
|
||||||
case batchReplicationJobError:
|
case batchReplicationJobError:
|
||||||
apiErr = APIError{
|
apiErr = APIError{
|
||||||
|
@ -95,6 +95,7 @@ func (e BatchJobKeyRotateEncryption) Validate() error {
|
|||||||
if e.Type == ssekms && spaces {
|
if e.Type == ssekms && spaces {
|
||||||
return crypto.ErrInvalidEncryptionKeyID
|
return crypto.ErrInvalidEncryptionKeyID
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Type == ssekms && GlobalKMS != nil {
|
if e.Type == ssekms && GlobalKMS != nil {
|
||||||
ctx := kms.Context{}
|
ctx := kms.Context{}
|
||||||
if e.Context != "" {
|
if e.Context != "" {
|
||||||
@ -113,7 +114,7 @@ func (e BatchJobKeyRotateEncryption) Validate() error {
|
|||||||
e.kmsContext[k] = v
|
e.kmsContext[k] = v
|
||||||
}
|
}
|
||||||
ctx["MinIO batch API"] = "batchrotate" // Context for a test key operation
|
ctx["MinIO batch API"] = "batchrotate" // Context for a test key operation
|
||||||
if _, err := GlobalKMS.GenerateKey(GlobalContext, e.Key, ctx); err != nil {
|
if _, err := GlobalKMS.GenerateKey(GlobalContext, &kms.GenerateKeyRequest{Name: e.Key, AssociatedData: ctx}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -478,8 +479,5 @@ func (r *BatchJobKeyRotateV1) Validate(ctx context.Context, job BatchJobRequest,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.Flags.Retry.Validate(); err != nil {
|
return r.Flags.Retry.Validate()
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r
|
|||||||
kmsKey := encConfig.KeyID()
|
kmsKey := encConfig.KeyID()
|
||||||
if kmsKey != "" {
|
if kmsKey != "" {
|
||||||
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
kmsContext := kms.Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
|
||||||
_, err := GlobalKMS.GenerateKey(ctx, kmsKey, kmsContext)
|
_, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{Name: kmsKey, AssociatedData: kmsContext})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
writeErrorResponse(ctx, w, toAPIError(ctx, errKMSKeyNotFound), r.URL)
|
writeErrorResponse(ctx, w, toAPIError(ctx, errKMSKeyNotFound), r.URL)
|
||||||
|
@ -490,7 +490,7 @@ func encryptBucketMetadata(ctx context.Context, bucket string, input []byte, kms
|
|||||||
}
|
}
|
||||||
|
|
||||||
metadata := make(map[string]string)
|
metadata := make(map[string]string)
|
||||||
key, err := GlobalKMS.GenerateKey(ctx, "", kmsContext)
|
key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{AssociatedData: kmsContext})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -519,7 +519,11 @@ func decryptBucketMetadata(input []byte, bucket string, meta map[string]string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
extKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, kmsContext)
|
extKey, err := GlobalKMS.Decrypt(context.TODO(), &kms.DecryptRequest{
|
||||||
|
Name: keyID,
|
||||||
|
Ciphertext: kmsKey,
|
||||||
|
AssociatedData: kmsContext,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,8 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
@ -49,7 +47,6 @@ import (
|
|||||||
"github.com/minio/console/api/operations"
|
"github.com/minio/console/api/operations"
|
||||||
consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2"
|
consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2"
|
||||||
consoleCerts "github.com/minio/console/pkg/certs"
|
consoleCerts "github.com/minio/console/pkg/certs"
|
||||||
"github.com/minio/kms-go/kes"
|
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
@ -60,7 +57,6 @@ import (
|
|||||||
"github.com/minio/minio/internal/logger"
|
"github.com/minio/minio/internal/logger"
|
||||||
"github.com/minio/pkg/v2/certs"
|
"github.com/minio/pkg/v2/certs"
|
||||||
"github.com/minio/pkg/v2/console"
|
"github.com/minio/pkg/v2/console"
|
||||||
"github.com/minio/pkg/v2/ellipses"
|
|
||||||
"github.com/minio/pkg/v2/env"
|
"github.com/minio/pkg/v2/env"
|
||||||
xnet "github.com/minio/pkg/v2/net"
|
xnet "github.com/minio/pkg/v2/net"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
@ -865,127 +861,28 @@ func loadRootCredentials() {
|
|||||||
// Initialize KMS global variable after valiadating and loading the configuration.
|
// Initialize KMS global variable after valiadating and loading the configuration.
|
||||||
// It depends on KMS env variables and global cli flags.
|
// It depends on KMS env variables and global cli flags.
|
||||||
func handleKMSConfig() {
|
func handleKMSConfig() {
|
||||||
if env.IsSet(kms.EnvKMSSecretKey) && env.IsSet(kms.EnvKESEndpoint) {
|
present, err := kms.IsPresent()
|
||||||
logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKMSSecretKey, kms.EnvKESEndpoint))
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Invalid KMS configuration specified")
|
||||||
|
}
|
||||||
|
if !present {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if env.IsSet(kms.EnvKMSSecretKey) {
|
KMS, err := kms.Connect(GlobalContext, &kms.ConnectionOptions{
|
||||||
KMS, err := kms.Parse(env.Get(kms.EnvKMSSecretKey, ""))
|
CADir: globalCertsCADir.Get(),
|
||||||
if err != nil {
|
})
|
||||||
logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment")
|
if err != nil {
|
||||||
}
|
logger.Fatal(err, "Failed to connect to KMS")
|
||||||
GlobalKMS = KMS
|
|
||||||
}
|
}
|
||||||
if env.IsSet(kms.EnvKESEndpoint) {
|
|
||||||
if env.IsSet(kms.EnvKESAPIKey) {
|
|
||||||
if env.IsSet(kms.EnvKESClientKey) {
|
|
||||||
logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientKey))
|
|
||||||
}
|
|
||||||
if env.IsSet(kms.EnvKESClientCert) {
|
|
||||||
logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKESAPIKey, kms.EnvKESClientCert))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !env.IsSet(kms.EnvKESKeyName) {
|
|
||||||
logger.Fatal(errors.New("Invalid KES configuration"), fmt.Sprintf("The mandatory environment variable %q not set", kms.EnvKESKeyName))
|
|
||||||
}
|
|
||||||
|
|
||||||
var endpoints []string
|
if _, err = KMS.GenerateKey(GlobalContext, &kms.GenerateKeyRequest{}); errors.Is(err, kms.ErrKeyNotFound) {
|
||||||
for _, endpoint := range strings.Split(env.Get(kms.EnvKESEndpoint, ""), ",") {
|
err = KMS.CreateKey(GlobalContext, &kms.CreateKeyRequest{Name: KMS.DefaultKey})
|
||||||
if strings.TrimSpace(endpoint) == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !ellipses.HasEllipses(endpoint) {
|
|
||||||
endpoints = append(endpoints, endpoint)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
patterns, err := ellipses.FindEllipsesPatterns(endpoint)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err, fmt.Sprintf("Invalid KES endpoint %q", endpoint))
|
|
||||||
}
|
|
||||||
for _, lbls := range patterns.Expand() {
|
|
||||||
endpoints = append(endpoints, strings.Join(lbls, ""))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rootCAs, err := certs.GetRootCAs(env.Get(kms.EnvKESServerCA, globalCertsCADir.Get()))
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err, fmt.Sprintf("Unable to load X.509 root CAs for KES from %q", env.Get(kms.EnvKESServerCA, globalCertsCADir.Get())))
|
|
||||||
}
|
|
||||||
|
|
||||||
var kmsConf kms.Config
|
|
||||||
if env.IsSet(kms.EnvKESAPIKey) {
|
|
||||||
key, err := kes.ParseAPIKey(env.Get(kms.EnvKESAPIKey, ""))
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err, fmt.Sprintf("Failed to parse KES API key from %q", env.Get(kms.EnvKESAPIKey, "")))
|
|
||||||
}
|
|
||||||
kmsConf = kms.Config{
|
|
||||||
Endpoints: endpoints,
|
|
||||||
DefaultKeyID: env.Get(kms.EnvKESKeyName, ""),
|
|
||||||
APIKey: key,
|
|
||||||
RootCAs: rootCAs,
|
|
||||||
}
|
|
||||||
} 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(kms.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
|
|
||||||
}
|
|
||||||
|
|
||||||
reloadCertEvents := make(chan tls.Certificate, 1)
|
|
||||||
certificate, err := certs.NewCertificate(env.Get(kms.EnvKESClientCert, ""), env.Get(kms.EnvKESClientKey, ""), loadX509KeyPair)
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err, "Failed to load KES client certificate")
|
|
||||||
}
|
|
||||||
certificate.Watch(context.Background(), 15*time.Minute, syscall.SIGHUP)
|
|
||||||
certificate.Notify(reloadCertEvents)
|
|
||||||
|
|
||||||
kmsConf = kms.Config{
|
|
||||||
Endpoints: endpoints,
|
|
||||||
DefaultKeyID: env.Get(kms.EnvKESKeyName, ""),
|
|
||||||
Certificate: certificate,
|
|
||||||
ReloadCertEvents: reloadCertEvents,
|
|
||||||
RootCAs: rootCAs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KMS, err := kms.NewWithConfig(kmsConf, KMSLogger{})
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
|
|
||||||
}
|
|
||||||
// Try to generate a data encryption key. Only try to create key if this fails.
|
|
||||||
// This implicitly checks that we can communicate to KES.
|
|
||||||
// We don't treat a policy error as failure condition since MinIO may not have the permission
|
|
||||||
// to create keys - just to generate/decrypt data encryption keys.
|
|
||||||
if _, err = KMS.GenerateKey(GlobalContext, env.Get(kms.EnvKESKeyName, ""), kms.Context{}); err != nil && errors.Is(err, kes.ErrKeyNotFound) {
|
|
||||||
if err = KMS.CreateKey(GlobalContext, env.Get(kms.EnvKESKeyName, "")); err != nil && !errors.Is(err, kes.ErrKeyExists) && !errors.Is(err, kes.ErrNotAllowed) {
|
|
||||||
logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
GlobalKMS = KMS
|
|
||||||
}
|
}
|
||||||
|
if err != nil && !errors.Is(err, kms.ErrKeyExists) && !errors.Is(err, kms.ErrPermission) {
|
||||||
|
logger.Fatal(err, "Failed to connect to KMS")
|
||||||
|
}
|
||||||
|
GlobalKMS = KMS
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
|
func getTLSConfig() (x509Certs []*x509.Certificate, manager *certs.Manager, secureConn bool, err error) {
|
||||||
|
@ -753,41 +753,33 @@ func autoGenerateRootCredentials() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if manager, ok := GlobalKMS.(kms.KeyManager); ok {
|
aKey, err := GlobalKMS.MAC(GlobalContext, &kms.MACRequest{Message: []byte("root access key")})
|
||||||
stat, err := GlobalKMS.Stat(GlobalContext)
|
if errors.Is(err, kes.ErrNotAllowed) || errors.Is(err, errors.ErrUnsupported) {
|
||||||
if err != nil {
|
return // If we don't have permission to compute the HMAC, don't change the cred.
|
||||||
kmsLogIf(GlobalContext, err, "Unable to generate root credentials using KMS")
|
}
|
||||||
return
|
if err != nil {
|
||||||
}
|
logger.Fatal(err, "Unable to generate root access key using KMS")
|
||||||
|
}
|
||||||
|
|
||||||
aKey, err := manager.HMAC(GlobalContext, stat.DefaultKey, []byte("root access key"))
|
sKey, err := GlobalKMS.MAC(GlobalContext, &kms.MACRequest{Message: []byte("root secret key")})
|
||||||
if errors.Is(err, kes.ErrNotAllowed) {
|
if err != nil {
|
||||||
return // If we don't have permission to compute the HMAC, don't change the cred.
|
// Here, we must have permission. Otherwise, we would have failed earlier.
|
||||||
}
|
logger.Fatal(err, "Unable to generate root secret key using KMS")
|
||||||
if err != nil {
|
}
|
||||||
logger.Fatal(err, "Unable to generate root access key using KMS")
|
|
||||||
}
|
|
||||||
|
|
||||||
sKey, err := manager.HMAC(GlobalContext, stat.DefaultKey, []byte("root secret key"))
|
accessKey, err := auth.GenerateAccessKey(20, bytes.NewReader(aKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Here, we must have permission. Otherwise, we would have failed earlier.
|
logger.Fatal(err, "Unable to generate root access key")
|
||||||
logger.Fatal(err, "Unable to generate root secret key using KMS")
|
}
|
||||||
}
|
secretKey, err := auth.GenerateSecretKey(32, bytes.NewReader(sKey))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal(err, "Unable to generate root secret key")
|
||||||
|
}
|
||||||
|
|
||||||
accessKey, err := auth.GenerateAccessKey(20, bytes.NewReader(aKey))
|
logger.Info("Automatically generated root access key and secret key with the KMS")
|
||||||
if err != nil {
|
globalActiveCred = auth.Credentials{
|
||||||
logger.Fatal(err, "Unable to generate root access key")
|
AccessKey: accessKey,
|
||||||
}
|
SecretKey: secretKey,
|
||||||
secretKey, err := auth.GenerateSecretKey(32, bytes.NewReader(sKey))
|
|
||||||
if err != nil {
|
|
||||||
logger.Fatal(err, "Unable to generate root secret key")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("Automatically generated root access key and secret key with the KMS")
|
|
||||||
globalActiveCred = auth.Credentials{
|
|
||||||
AccessKey: accessKey,
|
|
||||||
SecretKey: secretKey,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,7 +110,7 @@ func kmsKeyIDFromMetadata(metadata map[string]string) string {
|
|||||||
//
|
//
|
||||||
// DecryptETags uses a KMS bulk decryption API, if available, which
|
// DecryptETags uses a KMS bulk decryption API, if available, which
|
||||||
// is more efficient than decrypting ETags sequentually.
|
// is more efficient than decrypting ETags sequentually.
|
||||||
func DecryptETags(ctx context.Context, k kms.KMS, objects []ObjectInfo) error {
|
func DecryptETags(ctx context.Context, k *kms.KMS, objects []ObjectInfo) error {
|
||||||
const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.
|
const BatchSize = 250 // We process the objects in batches - 250 is a reasonable default.
|
||||||
var (
|
var (
|
||||||
metadata = make([]map[string]string, 0, BatchSize)
|
metadata = make([]map[string]string, 0, BatchSize)
|
||||||
@ -267,7 +267,11 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
oldKey, err := GlobalKMS.DecryptKey(keyID, kmsKey, kms.Context{bucket: path.Join(bucket, object)})
|
oldKey, err := GlobalKMS.Decrypt(ctx, &kms.DecryptRequest{
|
||||||
|
Name: keyID,
|
||||||
|
Ciphertext: kmsKey,
|
||||||
|
AssociatedData: kms.Context{bucket: path.Join(bucket, object)},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -276,7 +280,10 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
newKey, err := GlobalKMS.GenerateKey(ctx, "", kms.Context{bucket: path.Join(bucket, object)})
|
newKey, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
|
||||||
|
Name: GlobalKMS.DefaultKey,
|
||||||
|
AssociatedData: kms.Context{bucket: path.Join(bucket, object)},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -312,7 +319,10 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt
|
|||||||
if _, ok := kmsCtx[bucket]; !ok {
|
if _, ok := kmsCtx[bucket]; !ok {
|
||||||
kmsCtx[bucket] = path.Join(bucket, object)
|
kmsCtx[bucket] = path.Join(bucket, object)
|
||||||
}
|
}
|
||||||
newKey, err := GlobalKMS.GenerateKey(ctx, newKeyID, kmsCtx)
|
newKey, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
|
||||||
|
Name: newKeyID,
|
||||||
|
AssociatedData: kmsCtx,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -352,7 +362,9 @@ func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key
|
|||||||
if GlobalKMS == nil {
|
if GlobalKMS == nil {
|
||||||
return crypto.ObjectKey{}, errKMSNotConfigured
|
return crypto.ObjectKey{}, errKMSNotConfigured
|
||||||
}
|
}
|
||||||
key, err := GlobalKMS.GenerateKey(ctx, "", kms.Context{bucket: path.Join(bucket, object)})
|
key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
|
||||||
|
AssociatedData: kms.Context{bucket: path.Join(bucket, object)},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return crypto.ObjectKey{}, err
|
return crypto.ObjectKey{}, err
|
||||||
}
|
}
|
||||||
@ -379,7 +391,10 @@ func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key
|
|||||||
if _, ok := kmsCtx[bucket]; !ok {
|
if _, ok := kmsCtx[bucket]; !ok {
|
||||||
kmsCtx[bucket] = path.Join(bucket, object)
|
kmsCtx[bucket] = path.Join(bucket, object)
|
||||||
}
|
}
|
||||||
key, err := GlobalKMS.GenerateKey(ctx, keyID, kmsCtx)
|
key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{
|
||||||
|
Name: keyID,
|
||||||
|
AssociatedData: kmsCtx,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, kes.ErrKeyNotFound) {
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
return crypto.ObjectKey{}, errKMSKeyNotFound
|
return crypto.ObjectKey{}, errKMSKeyNotFound
|
||||||
@ -475,11 +490,10 @@ func EncryptRequest(content io.Reader, r *http.Request, bucket, object string, m
|
|||||||
func decryptObjectMeta(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
func decryptObjectMeta(key []byte, bucket, object string, metadata map[string]string) ([]byte, error) {
|
||||||
switch kind, _ := crypto.IsEncrypted(metadata); kind {
|
switch kind, _ := crypto.IsEncrypted(metadata); kind {
|
||||||
case crypto.S3:
|
case crypto.S3:
|
||||||
KMS := GlobalKMS
|
if GlobalKMS == nil {
|
||||||
if KMS == nil {
|
|
||||||
return nil, errKMSNotConfigured
|
return nil, errKMSNotConfigured
|
||||||
}
|
}
|
||||||
objectKey, err := crypto.S3.UnsealObjectKey(KMS, metadata, bucket, object)
|
objectKey, err := crypto.S3.UnsealObjectKey(GlobalKMS, metadata, bucket, object)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -349,7 +349,7 @@ var (
|
|||||||
globalDNSConfig dns.Store
|
globalDNSConfig dns.Store
|
||||||
|
|
||||||
// GlobalKMS initialized KMS configuration
|
// GlobalKMS initialized KMS configuration
|
||||||
GlobalKMS kms.KMS
|
GlobalKMS *kms.KMS
|
||||||
|
|
||||||
// Common lock for various subsystems performing the leader tasks
|
// Common lock for various subsystems performing the leader tasks
|
||||||
globalLeaderLock *sharedLock
|
globalLeaderLock *sharedLock
|
||||||
|
@ -135,7 +135,7 @@ func ReadinessCheckHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if _, err := GlobalKMS.GenerateKey(ctx, "", kms.Context{"healthcheck": ""}); err != nil {
|
if _, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{AssociatedData: kms.Context{"healthcheck": ""}}); err != nil {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodHead:
|
case http.MethodHead:
|
||||||
apiErr := toAPIError(r.Context(), err)
|
apiErr := toAPIError(r.Context(), err)
|
||||||
|
@ -20,10 +20,7 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/minio/kms-go/kes"
|
"github.com/minio/kms-go/kes"
|
||||||
"github.com/minio/madmin-go/v3"
|
"github.com/minio/madmin-go/v3"
|
||||||
@ -46,22 +43,12 @@ func (a kmsAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, err := GlobalKMS.Stat(ctx)
|
stat, err := GlobalKMS.Status(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
resp, err := json.Marshal(stat)
|
||||||
status := madmin.KMSStatus{
|
|
||||||
Name: stat.Name,
|
|
||||||
DefaultKeyID: stat.DefaultKey,
|
|
||||||
Endpoints: make(map[string]madmin.ItemState, len(stat.Endpoints)),
|
|
||||||
}
|
|
||||||
for _, endpoint := range stat.Endpoints {
|
|
||||||
status.Endpoints[endpoint] = madmin.ItemOnline // TODO(aead): Implement an online check for mTLS
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := json.Marshal(status)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
||||||
return
|
return
|
||||||
@ -84,11 +71,6 @@ func (a kmsAPIHandlers) KMSMetricsHandler(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := GlobalKMS.(kms.KeyManager); !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
metrics, err := GlobalKMS.Metrics(ctx)
|
metrics, err := GlobalKMS.Metrics(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
@ -116,13 +98,7 @@ func (a kmsAPIHandlers) KMSAPIsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, ok := GlobalKMS.(kms.StatusManager)
|
apis, err := GlobalKMS.APIs(ctx)
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apis, err := manager.APIs(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@ -153,13 +129,7 @@ func (a kmsAPIHandlers) KMSVersionHandler(w http.ResponseWriter, r *http.Request
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, ok := GlobalKMS.(kms.StatusManager)
|
version, err := GlobalKMS.Version(ctx)
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
version, err := manager.Version(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
@ -177,10 +147,6 @@ func (a kmsAPIHandlers) KMSVersionHandler(w http.ResponseWriter, r *http.Request
|
|||||||
func (a kmsAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Request) {
|
func (a kmsAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// If env variable MINIO_KMS_SECRET_KEY is populated, prevent creation of new keys
|
// If env variable MINIO_KMS_SECRET_KEY is populated, prevent creation of new keys
|
||||||
ctx := newContext(r, w, "KMSCreateKey")
|
ctx := newContext(r, w, "KMSCreateKey")
|
||||||
if GlobalKMS != nil && GlobalKMS.IsLocal() {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSDefaultKeyAlreadyConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSCreateKeyAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSCreateKeyAction)
|
||||||
@ -193,39 +159,7 @@ func (a kmsAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
manager, ok := GlobalKMS.(kms.KeyManager)
|
if err := GlobalKMS.CreateKey(ctx, &kms.CreateKeyRequest{Name: r.Form.Get("key-id")}); err != nil {
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := manager.CreateKey(ctx, r.Form.Get("key-id")); err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSDeleteKeyHandler - DELETE /minio/kms/v1/key/delete?key-id=<master-key-id>
|
|
||||||
func (a kmsAPIHandlers) KMSDeleteKeyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSDeleteKey")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDeleteKeyAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.KeyManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := manager.DeleteKey(ctx, r.Form.Get("key-id")); err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -235,15 +169,6 @@ func (a kmsAPIHandlers) KMSDeleteKeyHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
// KMSListKeysHandler - GET /minio/kms/v1/key/list?pattern=<pattern>
|
// KMSListKeysHandler - GET /minio/kms/v1/key/list?pattern=<pattern>
|
||||||
func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Request) {
|
func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "KMSListKeys")
|
ctx := newContext(r, w, "KMSListKeys")
|
||||||
if GlobalKMS != nil && GlobalKMS.IsLocal() {
|
|
||||||
res, err := json.Marshal(GlobalKMS.List())
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeSuccessResponseJSON(w, res)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListKeysAction)
|
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListKeysAction)
|
||||||
@ -255,28 +180,16 @@ func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
manager, ok := GlobalKMS.(kms.KeyManager)
|
names, _, err := GlobalKMS.ListKeyNames(ctx, &kms.ListRequest{
|
||||||
if !ok {
|
Prefix: r.Form.Get("pattern"),
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
keys, err := manager.ListKeys(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pattern := r.Form.Get("pattern")
|
values := make([]kes.KeyInfo, 0, len(names))
|
||||||
if !strings.Contains(pattern, "*") {
|
for _, name := range names {
|
||||||
pattern += "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
var values []kes.KeyInfo
|
|
||||||
for name, err := keys.SeekTo(ctx, pattern); err != io.EOF; name, err = keys.Next(ctx) {
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
values = append(values, kes.KeyInfo{
|
values = append(values, kes.KeyInfo{
|
||||||
Name: name,
|
Name: name,
|
||||||
})
|
})
|
||||||
@ -288,41 +201,6 @@ func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type importKeyRequest struct {
|
|
||||||
Bytes string
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSImportKeyHandler - POST /minio/kms/v1/key/import?key-id=<master-key-id>
|
|
||||||
func (a kmsAPIHandlers) KMSImportKeyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSImportKey")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSImportKeyAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.KeyManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var request importKeyRequest
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := manager.ImportKey(ctx, r.Form.Get("key-id"), []byte(request.Bytes)); err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeSuccessResponseHeadersOnly(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSKeyStatusHandler - GET /minio/kms/v1/key/status?key-id=<master-key-id>
|
// KMSKeyStatusHandler - GET /minio/kms/v1/key/status?key-id=<master-key-id>
|
||||||
func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
|
func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "KMSKeyStatus")
|
ctx := newContext(r, w, "KMSKeyStatus")
|
||||||
@ -338,15 +216,9 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stat, err := GlobalKMS.Stat(ctx)
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
keyID := r.Form.Get("key-id")
|
keyID := r.Form.Get("key-id")
|
||||||
if keyID == "" {
|
if keyID == "" {
|
||||||
keyID = stat.DefaultKey
|
keyID = GlobalKMS.DefaultKey
|
||||||
}
|
}
|
||||||
response := madmin.KMSKeyStatus{
|
response := madmin.KMSKeyStatus{
|
||||||
KeyID: keyID,
|
KeyID: keyID,
|
||||||
@ -354,7 +226,7 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
|
|
||||||
kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
kmsContext := kms.Context{"MinIO admin API": "KMSKeyStatusHandler"} // Context for a test key operation
|
||||||
// 1. Generate a new key using the KMS.
|
// 1. Generate a new key using the KMS.
|
||||||
key, err := GlobalKMS.GenerateKey(ctx, keyID, kmsContext)
|
key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{Name: keyID, AssociatedData: kmsContext})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.EncryptionErr = err.Error()
|
response.EncryptionErr = err.Error()
|
||||||
resp, err := json.Marshal(response)
|
resp, err := json.Marshal(response)
|
||||||
@ -367,7 +239,11 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Verify that we can indeed decrypt the (encrypted) key
|
// 2. Verify that we can indeed decrypt the (encrypted) key
|
||||||
decryptedKey, err := GlobalKMS.DecryptKey(key.KeyID, key.Ciphertext, kmsContext)
|
decryptedKey, err := GlobalKMS.Decrypt(ctx, &kms.DecryptRequest{
|
||||||
|
Name: key.KeyID,
|
||||||
|
Ciphertext: key.Ciphertext,
|
||||||
|
AssociatedData: kmsContext,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.DecryptionErr = err.Error()
|
response.DecryptionErr = err.Error()
|
||||||
resp, err := json.Marshal(response)
|
resp, err := json.Marshal(response)
|
||||||
@ -398,296 +274,3 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}
|
}
|
||||||
writeSuccessResponseJSON(w, resp)
|
writeSuccessResponseJSON(w, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMSDescribePolicyHandler - GET /minio/kms/v1/policy/describe?policy=<policy>
|
|
||||||
func (a kmsAPIHandlers) KMSDescribePolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSDescribePolicy")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDescribePolicyAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.PolicyManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
policy, err := manager.DescribePolicy(ctx, r.Form.Get("policy"))
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
p, err := json.Marshal(policy)
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeSuccessResponseJSON(w, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSAssignPolicyHandler - POST /minio/kms/v1/policy/assign?policy=<policy>
|
|
||||||
func (a kmsAPIHandlers) KMSAssignPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSAssignPolicy")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSAssignPolicyAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSDeletePolicyHandler - DELETE /minio/kms/v1/policy/delete?policy=<policy>
|
|
||||||
func (a kmsAPIHandlers) KMSDeletePolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSDeletePolicy")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDeletePolicyAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSListPoliciesHandler - GET /minio/kms/v1/policy/list?pattern=<pattern>
|
|
||||||
func (a kmsAPIHandlers) KMSListPoliciesHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSListPolicies")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListPoliciesAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.PolicyManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
policies, err := manager.ListPolicies(ctx)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pattern := r.Form.Get("pattern")
|
|
||||||
if !strings.Contains(pattern, "*") {
|
|
||||||
pattern += "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
var values []kes.PolicyInfo
|
|
||||||
for name, err := policies.SeekTo(ctx, pattern); err != io.EOF; name, err = policies.Next(ctx) {
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
values = append(values, kes.PolicyInfo{
|
|
||||||
Name: name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if res, err := json.Marshal(values); err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
} else {
|
|
||||||
writeSuccessResponseJSON(w, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSGetPolicyHandler - GET /minio/kms/v1/policy/get?policy=<policy>
|
|
||||||
func (a kmsAPIHandlers) KMSGetPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSGetPolicy")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSGetPolicyAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.PolicyManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
policy, err := manager.GetPolicy(ctx, r.Form.Get("policy"))
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if p, err := json.Marshal(policy); err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
} else {
|
|
||||||
writeSuccessResponseJSON(w, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSDescribeIdentityHandler - GET /minio/kms/v1/identity/describe?identity=<identity>
|
|
||||||
func (a kmsAPIHandlers) KMSDescribeIdentityHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSDescribeIdentity")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDescribeIdentityAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.IdentityManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
identity, err := manager.DescribeIdentity(ctx, r.Form.Get("identity"))
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
i, err := json.Marshal(identity)
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeSuccessResponseJSON(w, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
type describeSelfIdentityResponse struct {
|
|
||||||
Policy *kes.Policy `json:"policy"`
|
|
||||||
PolicyName string `json:"policyName"`
|
|
||||||
Identity string `json:"identity"`
|
|
||||||
IsAdmin bool `json:"isAdmin"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
CreatedBy string `json:"createdBy"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSDescribeSelfIdentityHandler - GET /minio/kms/v1/identity/describe-self
|
|
||||||
func (a kmsAPIHandlers) KMSDescribeSelfIdentityHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSDescribeSelfIdentity")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDescribeSelfIdentityAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.IdentityManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
identity, policy, err := manager.DescribeSelfIdentity(ctx)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
res := &describeSelfIdentityResponse{
|
|
||||||
Policy: policy,
|
|
||||||
PolicyName: identity.Policy,
|
|
||||||
Identity: identity.Identity.String(),
|
|
||||||
IsAdmin: identity.IsAdmin,
|
|
||||||
CreatedAt: identity.CreatedAt,
|
|
||||||
CreatedBy: identity.CreatedBy.String(),
|
|
||||||
}
|
|
||||||
i, err := json.Marshal(res)
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeSuccessResponseJSON(w, i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSDeleteIdentityHandler - DELETE /minio/kms/v1/identity/delete?identity=<identity>
|
|
||||||
func (a kmsAPIHandlers) KMSDeleteIdentityHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSDeleteIdentity")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSDeleteIdentityAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// KMSListIdentitiesHandler - GET /minio/kms/v1/identity/list?pattern=<pattern>
|
|
||||||
func (a kmsAPIHandlers) KMSListIdentitiesHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := newContext(r, w, "KMSListIdentities")
|
|
||||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
|
||||||
|
|
||||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSListIdentitiesAction)
|
|
||||||
if objectAPI == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if GlobalKMS == nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
manager, ok := GlobalKMS.(kms.IdentityManager)
|
|
||||||
if !ok {
|
|
||||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
identities, err := manager.ListIdentities(ctx)
|
|
||||||
if err != nil {
|
|
||||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pattern := r.Form.Get("pattern")
|
|
||||||
if !strings.Contains(pattern, "*") {
|
|
||||||
pattern += "*"
|
|
||||||
}
|
|
||||||
|
|
||||||
var values []kes.IdentityInfo
|
|
||||||
for name, err := identities.SeekTo(ctx, pattern); err != io.EOF; name, err = identities.Next(ctx) {
|
|
||||||
if err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
values = append(values, kes.IdentityInfo{
|
|
||||||
Identity: name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if res, err := json.Marshal(values); err != nil {
|
|
||||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
|
|
||||||
} else {
|
|
||||||
writeSuccessResponseJSON(w, res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -57,23 +57,8 @@ func registerKMSRouter(router *mux.Router) {
|
|||||||
kmsRouter.Methods(http.MethodGet).Path(version + "/version").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSVersionHandler)))
|
kmsRouter.Methods(http.MethodGet).Path(version + "/version").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSVersionHandler)))
|
||||||
// KMS Key APIs
|
// KMS Key APIs
|
||||||
kmsRouter.Methods(http.MethodPost).Path(version+"/key/create").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSCreateKeyHandler))).Queries("key-id", "{key-id:.*}")
|
kmsRouter.Methods(http.MethodPost).Path(version+"/key/create").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSCreateKeyHandler))).Queries("key-id", "{key-id:.*}")
|
||||||
kmsRouter.Methods(http.MethodPost).Path(version+"/key/import").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSImportKeyHandler))).Queries("key-id", "{key-id:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodDelete).Path(version+"/key/delete").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSDeleteKeyHandler))).Queries("key-id", "{key-id:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version+"/key/list").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSListKeysHandler))).Queries("pattern", "{pattern:.*}")
|
kmsRouter.Methods(http.MethodGet).Path(version+"/key/list").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSListKeysHandler))).Queries("pattern", "{pattern:.*}")
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version + "/key/status").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSKeyStatusHandler)))
|
kmsRouter.Methods(http.MethodGet).Path(version + "/key/status").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSKeyStatusHandler)))
|
||||||
|
|
||||||
// KMS Policy APIs
|
|
||||||
kmsRouter.Methods(http.MethodPost).Path(version+"/policy/assign").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSAssignPolicyHandler))).Queries("policy", "{policy:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version+"/policy/describe").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSDescribePolicyHandler))).Queries("policy", "{policy:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version+"/policy/get").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSGetPolicyHandler))).Queries("policy", "{policy:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodDelete).Path(version+"/policy/delete").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSDeletePolicyHandler))).Queries("policy", "{policy:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version+"/policy/list").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSListPoliciesHandler))).Queries("pattern", "{pattern:.*}")
|
|
||||||
|
|
||||||
// KMS Identity APIs
|
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version+"/identity/describe").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSDescribeIdentityHandler))).Queries("identity", "{identity:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version + "/identity/describe-self").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSDescribeSelfIdentityHandler)))
|
|
||||||
kmsRouter.Methods(http.MethodDelete).Path(version+"/identity/delete").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSDeleteIdentityHandler))).Queries("identity", "{identity:.*}")
|
|
||||||
kmsRouter.Methods(http.MethodGet).Path(version+"/identity/list").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSListIdentitiesHandler))).Queries("pattern", "{pattern:.*}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If none of the routes match add default error handler routes
|
// If none of the routes match add default error handler routes
|
||||||
|
@ -3970,7 +3970,7 @@ func getKMSMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
|
|||||||
Help: "Number of KMS requests that succeeded",
|
Help: "Number of KMS requests that succeeded",
|
||||||
Type: counterMetric,
|
Type: counterMetric,
|
||||||
},
|
},
|
||||||
Value: float64(metric.RequestOK),
|
Value: float64(metric.ReqOK),
|
||||||
})
|
})
|
||||||
metrics = append(metrics, MetricV2{
|
metrics = append(metrics, MetricV2{
|
||||||
Description: MetricDescription{
|
Description: MetricDescription{
|
||||||
@ -3980,7 +3980,7 @@ func getKMSMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
|
|||||||
Help: "Number of KMS requests that failed due to some error. (HTTP 4xx status code)",
|
Help: "Number of KMS requests that failed due to some error. (HTTP 4xx status code)",
|
||||||
Type: counterMetric,
|
Type: counterMetric,
|
||||||
},
|
},
|
||||||
Value: float64(metric.RequestErr),
|
Value: float64(metric.ReqErr),
|
||||||
})
|
})
|
||||||
metrics = append(metrics, MetricV2{
|
metrics = append(metrics, MetricV2{
|
||||||
Description: MetricDescription{
|
Description: MetricDescription{
|
||||||
@ -3990,19 +3990,8 @@ func getKMSMetrics(opts MetricsGroupOpts) *MetricsGroupV2 {
|
|||||||
Help: "Number of KMS requests that failed due to some internal failure. (HTTP 5xx status code)",
|
Help: "Number of KMS requests that failed due to some internal failure. (HTTP 5xx status code)",
|
||||||
Type: counterMetric,
|
Type: counterMetric,
|
||||||
},
|
},
|
||||||
Value: float64(metric.RequestFail),
|
Value: float64(metric.ReqFail),
|
||||||
})
|
})
|
||||||
metrics = append(metrics, MetricV2{
|
|
||||||
Description: MetricDescription{
|
|
||||||
Namespace: clusterMetricNamespace,
|
|
||||||
Subsystem: kmsSubsystem,
|
|
||||||
Name: kmsUptime,
|
|
||||||
Help: "The time the KMS has been up and running in seconds.",
|
|
||||||
Type: counterMetric,
|
|
||||||
},
|
|
||||||
Value: metric.UpTime.Seconds(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return metrics
|
return metrics
|
||||||
})
|
})
|
||||||
return mg
|
return mg
|
||||||
|
@ -521,11 +521,11 @@ func enableCompression(t *testing.T, encrypt bool) {
|
|||||||
globalCompressConfigMu.Unlock()
|
globalCompressConfigMu.Unlock()
|
||||||
if encrypt {
|
if encrypt {
|
||||||
globalAutoEncryption = encrypt
|
globalAutoEncryption = encrypt
|
||||||
var err error
|
KMS, err := kms.ParseSecretKey("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=")
|
||||||
GlobalKMS, err = kms.Parse("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
GlobalKMS = KMS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,11 +536,11 @@ func enableEncryption(t *testing.T) {
|
|||||||
globalCompressConfigMu.Unlock()
|
globalCompressConfigMu.Unlock()
|
||||||
|
|
||||||
globalAutoEncryption = true
|
globalAutoEncryption = true
|
||||||
var err error
|
KMS, err := kms.ParseSecretKey("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=")
|
||||||
GlobalKMS, err = kms.Parse("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
GlobalKMS = KMS
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetCompressEncryption() {
|
func resetCompressEncryption() {
|
||||||
|
@ -856,12 +856,7 @@ func (c *SiteReplicationSys) MakeBucketHook(ctx context.Context, bucket string,
|
|||||||
if err := errors.Unwrap(makeBucketConcErr); err != nil {
|
if err := errors.Unwrap(makeBucketConcErr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return errors.Unwrap(makeRemotesConcErr)
|
||||||
if err := errors.Unwrap(makeRemotesConcErr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteBucketHook - called during a regular delete bucket call when cluster
|
// DeleteBucketHook - called during a regular delete bucket call when cluster
|
||||||
|
1
go.mod
1
go.mod
@ -51,6 +51,7 @@ require (
|
|||||||
github.com/minio/dperf v0.5.3
|
github.com/minio/dperf v0.5.3
|
||||||
github.com/minio/highwayhash v1.0.2
|
github.com/minio/highwayhash v1.0.2
|
||||||
github.com/minio/kms-go/kes v0.3.0
|
github.com/minio/kms-go/kes v0.3.0
|
||||||
|
github.com/minio/kms-go/kms v0.4.0
|
||||||
github.com/minio/madmin-go/v3 v3.0.51
|
github.com/minio/madmin-go/v3 v3.0.51
|
||||||
github.com/minio/minio-go/v7 v7.0.70
|
github.com/minio/minio-go/v7 v7.0.70
|
||||||
github.com/minio/mux v1.9.0
|
github.com/minio/mux v1.9.0
|
||||||
|
2
go.sum
2
go.sum
@ -438,6 +438,8 @@ github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA
|
|||||||
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=
|
||||||
github.com/minio/kms-go/kes v0.3.0 h1:SU8VGVM/Hk9w1OiSby3OatkcojooUqIdDHl6dtM6NkY=
|
github.com/minio/kms-go/kes v0.3.0 h1:SU8VGVM/Hk9w1OiSby3OatkcojooUqIdDHl6dtM6NkY=
|
||||||
github.com/minio/kms-go/kes v0.3.0/go.mod h1:w6DeVT878qEOU3nUrYVy1WOT5H1Ig9hbDIh698NYJKY=
|
github.com/minio/kms-go/kes v0.3.0/go.mod h1:w6DeVT878qEOU3nUrYVy1WOT5H1Ig9hbDIh698NYJKY=
|
||||||
|
github.com/minio/kms-go/kms v0.4.0 h1:cLPZceEp+05xHotVBaeFJrgL7JcXM4lBy6PU0idkE7I=
|
||||||
|
github.com/minio/kms-go/kms v0.4.0/go.mod h1:q12CehiIy2qgBnDKq6Q7wmPi2PHSyRVug5DKp0HAVeE=
|
||||||
github.com/minio/madmin-go/v3 v3.0.51 h1:brGOvDP8KvoHb/bdzCHUPFCbTtrN8o507uPHZpyuinM=
|
github.com/minio/madmin-go/v3 v3.0.51 h1:brGOvDP8KvoHb/bdzCHUPFCbTtrN8o507uPHZpyuinM=
|
||||||
github.com/minio/madmin-go/v3 v3.0.51/go.mod h1:IFAwr0XMrdsLovxAdCcuq/eoL4nRuMVQQv0iubJANQw=
|
github.com/minio/madmin-go/v3 v3.0.51/go.mod h1:IFAwr0XMrdsLovxAdCcuq/eoL4nRuMVQQv0iubJANQw=
|
||||||
github.com/minio/mc v0.0.0-20240430174448-dcb911bed9d5 h1:VDXLzvY0Jxk4lzIntGXZuw0VH7S1JgQBmjWGkz7xphU=
|
github.com/minio/mc v0.0.0-20240430174448-dcb911bed9d5 h1:VDXLzvY0Jxk4lzIntGXZuw0VH7S1JgQBmjWGkz7xphU=
|
||||||
|
@ -38,7 +38,7 @@ import (
|
|||||||
//
|
//
|
||||||
// The same context must be provided when decrypting the
|
// The same context must be provided when decrypting the
|
||||||
// ciphertext.
|
// ciphertext.
|
||||||
func EncryptBytes(k kms.KMS, plaintext []byte, context kms.Context) ([]byte, error) {
|
func EncryptBytes(k *kms.KMS, plaintext []byte, context kms.Context) ([]byte, error) {
|
||||||
ciphertext, err := Encrypt(k, bytes.NewReader(plaintext), context)
|
ciphertext, err := Encrypt(k, bytes.NewReader(plaintext), context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -49,7 +49,7 @@ func EncryptBytes(k kms.KMS, plaintext []byte, context kms.Context) ([]byte, err
|
|||||||
// DecryptBytes decrypts the ciphertext using a key managed by the KMS.
|
// DecryptBytes decrypts the ciphertext using a key managed by the KMS.
|
||||||
// The same context that have been used during encryption must be
|
// The same context that have been used during encryption must be
|
||||||
// provided.
|
// provided.
|
||||||
func DecryptBytes(k kms.KMS, ciphertext []byte, context kms.Context) ([]byte, error) {
|
func DecryptBytes(k *kms.KMS, ciphertext []byte, context kms.Context) ([]byte, error) {
|
||||||
plaintext, err := Decrypt(k, bytes.NewReader(ciphertext), context)
|
plaintext, err := Decrypt(k, bytes.NewReader(ciphertext), context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -62,13 +62,13 @@ func DecryptBytes(k kms.KMS, ciphertext []byte, context kms.Context) ([]byte, er
|
|||||||
//
|
//
|
||||||
// The same context must be provided when decrypting the
|
// The same context must be provided when decrypting the
|
||||||
// ciphertext.
|
// ciphertext.
|
||||||
func Encrypt(k kms.KMS, plaintext io.Reader, ctx kms.Context) (io.Reader, error) {
|
func Encrypt(k *kms.KMS, plaintext io.Reader, ctx kms.Context) (io.Reader, error) {
|
||||||
algorithm := sio.AES_256_GCM
|
algorithm := sio.AES_256_GCM
|
||||||
if !fips.Enabled && !sioutil.NativeAES() {
|
if !fips.Enabled && !sioutil.NativeAES() {
|
||||||
algorithm = sio.ChaCha20Poly1305
|
algorithm = sio.ChaCha20Poly1305
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := k.GenerateKey(context.Background(), "", ctx)
|
key, err := k.GenerateKey(context.Background(), &kms.GenerateKeyRequest{AssociatedData: ctx})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@ func Encrypt(k kms.KMS, plaintext io.Reader, ctx kms.Context) (io.Reader, error)
|
|||||||
// Decrypt decrypts the ciphertext using a key managed by the KMS.
|
// Decrypt decrypts the ciphertext using a key managed by the KMS.
|
||||||
// The same context that have been used during encryption must be
|
// The same context that have been used during encryption must be
|
||||||
// provided.
|
// provided.
|
||||||
func Decrypt(k kms.KMS, ciphertext io.Reader, context kms.Context) (io.Reader, error) {
|
func Decrypt(k *kms.KMS, ciphertext io.Reader, associatedData kms.Context) (io.Reader, error) {
|
||||||
const (
|
const (
|
||||||
MaxMetadataSize = 1 << 20 // max. size of the metadata
|
MaxMetadataSize = 1 << 20 // max. size of the metadata
|
||||||
Version = 1
|
Version = 1
|
||||||
@ -149,7 +149,11 @@ func Decrypt(k kms.KMS, ciphertext io.Reader, context kms.Context) (io.Reader, e
|
|||||||
return nil, fmt.Errorf("config: unsupported encryption algorithm: %q is not supported in FIPS mode", metadata.Algorithm)
|
return nil, fmt.Errorf("config: unsupported encryption algorithm: %q is not supported in FIPS mode", metadata.Algorithm)
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := k.DecryptKey(metadata.KeyID, metadata.KMSKey, context)
|
key, err := k.Decrypt(context.TODO(), &kms.DecryptRequest{
|
||||||
|
Name: metadata.KeyID,
|
||||||
|
Ciphertext: metadata.KMSKey,
|
||||||
|
AssociatedData: associatedData,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ func TestEncryptDecrypt(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to decode master key: %v", err)
|
t.Fatalf("Failed to decode master key: %v", err)
|
||||||
}
|
}
|
||||||
KMS, err := kms.New("my-key", key)
|
KMS, err := kms.NewBuiltin("my-key", key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create KMS: %v", err)
|
t.Fatalf("Failed to create KMS: %v", err)
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ func BenchmarkEncrypt(b *testing.B) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed to decode master key: %v", err)
|
b.Fatalf("Failed to decode master key: %v", err)
|
||||||
}
|
}
|
||||||
KMS, err := kms.New("my-key", key)
|
KMS, err := kms.NewBuiltin("my-key", key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatalf("Failed to create KMS: %v", err)
|
b.Fatalf("Failed to create KMS: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,7 @@ func (ssekms) IsEncrypted(metadata map[string]string) bool {
|
|||||||
// UnsealObjectKey extracts and decrypts the sealed object key
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
// from the metadata using KMS and returns the decrypted object
|
// from the metadata using KMS and returns the decrypted object
|
||||||
// key.
|
// key.
|
||||||
func (s3 ssekms) UnsealObjectKey(k kms.KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
func (s3 ssekms) UnsealObjectKey(k *kms.KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
if k == nil {
|
if k == nil {
|
||||||
return key, Errorf("KMS not configured")
|
return key, Errorf("KMS not configured")
|
||||||
}
|
}
|
||||||
@ -120,7 +120,11 @@ func (s3 ssekms) UnsealObjectKey(k kms.KMS, metadata map[string]string, bucket,
|
|||||||
} else if _, ok := ctx[bucket]; !ok {
|
} else if _, ok := ctx[bucket]; !ok {
|
||||||
ctx[bucket] = path.Join(bucket, object)
|
ctx[bucket] = path.Join(bucket, object)
|
||||||
}
|
}
|
||||||
unsealKey, err := k.DecryptKey(keyID, kmsKey, ctx)
|
unsealKey, err := k.Decrypt(context.TODO(), &kms.DecryptRequest{
|
||||||
|
Name: keyID,
|
||||||
|
Ciphertext: kmsKey,
|
||||||
|
AssociatedData: ctx,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return key, err
|
return key, err
|
||||||
}
|
}
|
||||||
|
@ -71,7 +71,7 @@ func (sses3) IsEncrypted(metadata map[string]string) bool {
|
|||||||
// UnsealObjectKey extracts and decrypts the sealed object key
|
// UnsealObjectKey extracts and decrypts the sealed object key
|
||||||
// from the metadata using KMS and returns the decrypted object
|
// from the metadata using KMS and returns the decrypted object
|
||||||
// key.
|
// key.
|
||||||
func (s3 sses3) UnsealObjectKey(k kms.KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
func (s3 sses3) UnsealObjectKey(k *kms.KMS, metadata map[string]string, bucket, object string) (key ObjectKey, err error) {
|
||||||
if k == nil {
|
if k == nil {
|
||||||
return key, Errorf("KMS not configured")
|
return key, Errorf("KMS not configured")
|
||||||
}
|
}
|
||||||
@ -79,7 +79,11 @@ func (s3 sses3) UnsealObjectKey(k kms.KMS, metadata map[string]string, bucket, o
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return key, err
|
return key, err
|
||||||
}
|
}
|
||||||
unsealKey, err := k.DecryptKey(keyID, kmsKey, kms.Context{bucket: path.Join(bucket, object)})
|
unsealKey, err := k.Decrypt(context.TODO(), &kms.DecryptRequest{
|
||||||
|
Name: keyID,
|
||||||
|
Ciphertext: kmsKey,
|
||||||
|
AssociatedData: kms.Context{bucket: path.Join(bucket, object)},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return key, err
|
return key, err
|
||||||
}
|
}
|
||||||
@ -92,7 +96,7 @@ func (s3 sses3) UnsealObjectKey(k kms.KMS, metadata map[string]string, bucket, o
|
|||||||
// keys.
|
// keys.
|
||||||
//
|
//
|
||||||
// The metadata, buckets and objects slices must have the same length.
|
// The metadata, buckets and objects slices must have the same length.
|
||||||
func (s3 sses3) UnsealObjectKeys(ctx context.Context, k kms.KMS, metadata []map[string]string, buckets, objects []string) ([]ObjectKey, error) {
|
func (s3 sses3) UnsealObjectKeys(ctx context.Context, k *kms.KMS, metadata []map[string]string, buckets, objects []string) ([]ObjectKey, error) {
|
||||||
if k == nil {
|
if k == nil {
|
||||||
return nil, Errorf("KMS not configured")
|
return nil, Errorf("KMS not configured")
|
||||||
}
|
}
|
||||||
@ -100,45 +104,8 @@ func (s3 sses3) UnsealObjectKeys(ctx context.Context, k kms.KMS, metadata []map[
|
|||||||
if len(metadata) != len(buckets) || len(metadata) != len(objects) {
|
if len(metadata) != len(buckets) || len(metadata) != len(objects) {
|
||||||
return nil, Errorf("invalid metadata/object count: %d != %d != %d", len(metadata), len(buckets), len(objects))
|
return nil, Errorf("invalid metadata/object count: %d != %d != %d", len(metadata), len(buckets), len(objects))
|
||||||
}
|
}
|
||||||
|
keys := make([]ObjectKey, 0, len(metadata))
|
||||||
keyIDs := make([]string, 0, len(metadata))
|
|
||||||
kmsKeys := make([][]byte, 0, len(metadata))
|
|
||||||
sealedKeys := make([]SealedKey, 0, len(metadata))
|
|
||||||
|
|
||||||
sameKeyID := true
|
|
||||||
for i := range metadata {
|
for i := range metadata {
|
||||||
keyID, kmsKey, sealedKey, err := s3.ParseMetadata(metadata[i])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keyIDs = append(keyIDs, keyID)
|
|
||||||
kmsKeys = append(kmsKeys, kmsKey)
|
|
||||||
sealedKeys = append(sealedKeys, sealedKey)
|
|
||||||
|
|
||||||
if i > 0 && keyID != keyIDs[i-1] {
|
|
||||||
sameKeyID = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if sameKeyID {
|
|
||||||
contexts := make([]kms.Context, 0, len(keyIDs))
|
|
||||||
for i := range buckets {
|
|
||||||
contexts = append(contexts, kms.Context{buckets[i]: path.Join(buckets[i], objects[i])})
|
|
||||||
}
|
|
||||||
unsealKeys, err := k.DecryptAll(ctx, keyIDs[0], kmsKeys, contexts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
keys := make([]ObjectKey, len(unsealKeys))
|
|
||||||
for i := range keys {
|
|
||||||
if err := keys[i].Unseal(unsealKeys[i], sealedKeys[i], s3.String(), buckets[i], objects[i]); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keys, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := make([]ObjectKey, 0, len(keyIDs))
|
|
||||||
for i := range keyIDs {
|
|
||||||
key, err := s3.UnsealObjectKey(k, metadata[i], buckets[i], objects[i])
|
key, err := s3.UnsealObjectKey(k, metadata[i], buckets[i], objects[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -17,16 +17,393 @@
|
|||||||
|
|
||||||
package kms
|
package kms
|
||||||
|
|
||||||
// Top level config constants for KMS
|
import (
|
||||||
const (
|
"bytes"
|
||||||
EnvKMSSecretKey = "MINIO_KMS_SECRET_KEY"
|
"context"
|
||||||
EnvKMSSecretKeyFile = "MINIO_KMS_SECRET_KEY_FILE"
|
"crypto/tls"
|
||||||
EnvKESEndpoint = "MINIO_KMS_KES_ENDPOINT" // One or multiple KES endpoints, separated by ','
|
"crypto/x509"
|
||||||
EnvKESKeyName = "MINIO_KMS_KES_KEY_NAME" // The default key name used for IAM data and when no key ID is specified on a bucket
|
"encoding/pem"
|
||||||
EnvKESAPIKey = "MINIO_KMS_KES_API_KEY" // Access credential for KES - API keys and private key / certificate are mutually exclusive
|
"errors"
|
||||||
EnvKESClientKey = "MINIO_KMS_KES_KEY_FILE" // Path to TLS private key for authenticating to KES with mTLS - usually prefer API keys
|
"fmt"
|
||||||
EnvKESClientPassword = "MINIO_KMS_KES_KEY_PASSWORD" // Optional password to decrypt an encrypt TLS private key
|
"os"
|
||||||
EnvKESClientCert = "MINIO_KMS_KES_CERT_FILE" // Path to TLS certificate for authenticating to KES with mTLS - usually prefer API keys
|
"strings"
|
||||||
EnvKESServerCA = "MINIO_KMS_KES_CAPATH" // Path to file/directory containing CA certificates to verify the KES server certificate
|
"sync/atomic"
|
||||||
EnvKESKeyCacheInterval = "MINIO_KMS_KEY_CACHE_INTERVAL" // Period between polls of the KES KMS Master Key cache, to prevent it from being unused and purged
|
"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
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 {
|
||||||
|
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
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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -41,6 +41,13 @@ var dekEncodeDecodeTests = []struct {
|
|||||||
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
|
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Key: DEK{
|
||||||
|
Version: 3,
|
||||||
|
Plaintext: mustDecodeB64("GM2UvLXp/X8lzqq0mibFC0LayDCGlmTHQhYLj7qAy7Q="),
|
||||||
|
Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="),
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeDecodeDEK(t *testing.T) {
|
func TestEncodeDecodeDEK(t *testing.T) {
|
||||||
|
@ -17,13 +17,112 @@
|
|||||||
|
|
||||||
package kms
|
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 {
|
type Error struct {
|
||||||
Err error
|
Code int // The HTTP status code returned to the client
|
||||||
APICode string
|
APICode string // The API error code identifying the error
|
||||||
HTTPStatusCode int
|
Err string // The error message returned to the client
|
||||||
|
Cause error // Optional, lower level error cause.
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Error) Error() string {
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -18,239 +18,116 @@
|
|||||||
package kms
|
package kms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/subtle"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"net/http"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/pkg/v2/env"
|
|
||||||
|
|
||||||
"github.com/minio/kms-go/kes"
|
"github.com/minio/kms-go/kes"
|
||||||
"github.com/minio/pkg/v2/certs"
|
"github.com/minio/madmin-go/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type kesConn struct {
|
||||||
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
|
|
||||||
defaultKeyID string
|
defaultKeyID string
|
||||||
client *kes.Client
|
client *kes.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
var ( // compiler checks
|
func (c *kesConn) Version(ctx context.Context) (string, error) {
|
||||||
_ 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()
|
|
||||||
|
|
||||||
return c.client.Version(ctx)
|
return c.client.Version(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIs retrieves a list of supported API endpoints
|
func (c *kesConn) APIs(ctx context.Context) ([]madmin.KMSAPI, error) {
|
||||||
func (c *kesClient) APIs(ctx context.Context) ([]kes.API, error) {
|
APIs, err := c.client.APIs(ctx)
|
||||||
c.lock.RLock()
|
if err != nil {
|
||||||
defer c.lock.RUnlock()
|
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
|
// 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
|
// If the a key with the same keyID already exists then
|
||||||
// CreateKey returns kes.ErrKeyExists.
|
// CreateKey returns kes.ErrKeyExists.
|
||||||
func (c *kesClient) CreateKey(ctx context.Context, keyID string) error {
|
func (c *kesConn) CreateKey(ctx context.Context, req *CreateKeyRequest) error {
|
||||||
c.lock.RLock()
|
if err := c.client.CreateKey(ctx, req.Name); err != nil {
|
||||||
defer c.lock.RUnlock()
|
if errors.Is(err, kes.ErrKeyExists) {
|
||||||
|
return ErrKeyExists
|
||||||
return c.client.CreateKey(ctx, keyID)
|
}
|
||||||
|
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.
|
// DeleteKey deletes a key at the KMS with the given key ID.
|
||||||
// Please note that is a dangerous operation.
|
// Please note that is a dangerous operation.
|
||||||
// Once a key has been deleted all data that has been encrypted with it cannot be decrypted
|
// Once a key has been deleted all data that has been encrypted with it cannot be decrypted
|
||||||
// anymore, and therefore, is lost.
|
// anymore, and therefore, is lost.
|
||||||
func (c *kesClient) DeleteKey(ctx context.Context, keyID string) error {
|
func (c *kesConn) DeleteKey(ctx context.Context, req *DeleteKeyRequest) error {
|
||||||
c.lock.RLock()
|
if err := c.client.DeleteKey(ctx, req.Name); err != nil {
|
||||||
defer c.lock.RUnlock()
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
|
return ErrKeyNotFound
|
||||||
return c.client.DeleteKey(ctx, keyID)
|
}
|
||||||
}
|
if errors.Is(err, kes.ErrNotAllowed) {
|
||||||
|
return ErrPermission
|
||||||
// ListKeys returns an iterator over all key names.
|
}
|
||||||
func (c *kesClient) ListKeys(ctx context.Context) (*kes.ListIter[string], error) {
|
return errKeyDeletionFailed(err)
|
||||||
c.lock.RLock()
|
}
|
||||||
defer c.lock.RUnlock()
|
return nil
|
||||||
|
|
||||||
return &kes.ListIter[string]{
|
|
||||||
NextFunc: c.client.ListKeys,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateKey generates a new data encryption key using
|
// 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 context is associated and tied to the generated DEK.
|
||||||
// The same context must be provided when the generated
|
// The same context must be provided when the generated
|
||||||
// key should be decrypted.
|
// key should be decrypted.
|
||||||
func (c *kesClient) GenerateKey(ctx context.Context, keyID string, cryptoCtx Context) (DEK, error) {
|
func (c *kesConn) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) {
|
||||||
c.lock.RLock()
|
aad, err := req.AssociatedData.MarshalText()
|
||||||
defer c.lock.RUnlock()
|
|
||||||
|
|
||||||
if keyID == "" {
|
|
||||||
keyID = c.defaultKeyID
|
|
||||||
}
|
|
||||||
ctxBytes, err := cryptoCtx.MarshalText()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return DEK{}, err
|
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 {
|
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{
|
return DEK{
|
||||||
KeyID: keyID,
|
KeyID: name,
|
||||||
Plaintext: dek.Plaintext,
|
Plaintext: dek.Plaintext,
|
||||||
Ciphertext: dek.Ciphertext,
|
Ciphertext: dek.Ciphertext,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImportKey imports a cryptographic key into the KMS.
|
// ImportKey imports a cryptographic key into the KMS.
|
||||||
func (c *kesClient) ImportKey(ctx context.Context, keyID string, bytes []byte) error {
|
func (c *kesConn) ImportKey(ctx context.Context, keyID string, bytes []byte) error {
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
|
|
||||||
return c.client.ImportKey(ctx, keyID, &kes.ImportKeyRequest{
|
return c.client.ImportKey(ctx, keyID, &kes.ImportKeyRequest{
|
||||||
Key: bytes,
|
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
|
// EncryptKey Encrypts and authenticates a (small) plaintext with the cryptographic key
|
||||||
// The plaintext must not exceed 1 MB
|
// The plaintext must not exceed 1 MB
|
||||||
func (c *kesClient) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) {
|
func (c *kesConn) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) {
|
||||||
c.lock.RLock()
|
|
||||||
defer c.lock.RUnlock()
|
|
||||||
|
|
||||||
ctxBytes, err := ctx.MarshalText()
|
ctxBytes, err := ctx.MarshalText()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// DecryptKey decrypts the ciphertext with the key at the KES
|
||||||
// server referenced by the key ID. The context must match the
|
// server referenced by the key ID. The context must match the
|
||||||
// context value used to generate the ciphertext.
|
// context value used to generate the ciphertext.
|
||||||
func (c *kesClient) DecryptKey(keyID string, ciphertext []byte, ctx Context) ([]byte, error) {
|
func (c *kesConn) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) {
|
||||||
c.lock.RLock()
|
aad, err := req.AssociatedData.MarshalText()
|
||||||
defer c.lock.RUnlock()
|
|
||||||
|
|
||||||
ctxBytes, err := ctx.MarshalText()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
plaintext, err := c.client.Decrypt(context.Background(), req.Name, req.Ciphertext, aad)
|
||||||
c.lock.RLock()
|
if err != nil {
|
||||||
defer c.lock.RUnlock()
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
|
return nil, ErrKeyNotFound
|
||||||
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(ctx, keyID, ciphertexts[i], ctxBytes)
|
if errors.Is(err, kes.ErrDecrypt) {
|
||||||
if err != nil {
|
return nil, ErrDecrypt
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
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
|
// MAC generates the checksum of the given req.Message using the key
|
||||||
// with the given keyID at the KMS.
|
// with the req.Name at the KMS.
|
||||||
func (c *kesClient) HMAC(ctx context.Context, keyID string, msg []byte) ([]byte, error) {
|
func (c *kesConn) MAC(ctx context.Context, req *MACRequest) ([]byte, error) {
|
||||||
c.lock.RLock()
|
mac, err := c.client.HMAC(context.Background(), req.Name, req.Message)
|
||||||
defer c.lock.RUnlock()
|
if err != nil {
|
||||||
|
if errors.Is(err, kes.ErrKeyNotFound) {
|
||||||
return c.client.HMAC(context.Background(), keyID, msg)
|
return nil, ErrKeyNotFound
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, kes.ErrNotAllowed) {
|
||||||
// 1. Get stats for the KES instance
|
return nil, ErrPermission
|
||||||
state, err := client.Status(ctx)
|
|
||||||
if err != nil {
|
|
||||||
results = append(results, VerifyResult{Status: "offline", Endpoint: endpoint})
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
if kErr, ok := err.(kes.Error); ok && kErr.Status() == http.StatusNotImplemented {
|
||||||
// 2. Generate a new key using the KMS.
|
return nil, ErrNotSupported
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return validKey
|
return mac, nil
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -19,132 +19,403 @@ package kms
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding"
|
"errors"
|
||||||
"encoding/json"
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
jsoniter "github.com/json-iterator/go"
|
"github.com/minio/kms-go/kms"
|
||||||
"github.com/minio/kms-go/kes"
|
"github.com/minio/madmin-go/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KMS is the generic interface that abstracts over
|
// ListRequest is a structure containing fields
|
||||||
// different KMS implementations.
|
// and options for listing keys.
|
||||||
type KMS interface {
|
type ListRequest struct {
|
||||||
// Stat returns the current KMS status.
|
// Prefix is an optional prefix for filtering names.
|
||||||
Stat(cxt context.Context) (Status, error)
|
// 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
|
// ContinueAt is the name of the element from where
|
||||||
IsLocal() bool
|
// a listing should continue. It allows paginated
|
||||||
|
// listings.
|
||||||
|
ContinueAt string
|
||||||
|
|
||||||
// List returns an array of local KMS Names
|
// Limit limits the number of elements returned by
|
||||||
List() []kes.KeyInfo
|
// a single list operation. If <= 0, a reasonable
|
||||||
|
// limit is selected automatically.
|
||||||
// Metrics returns a KMS metric snapshot.
|
Limit int
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyResult describes the verification result details a KMS endpoint
|
// CreateKeyRequest is a structure containing fields
|
||||||
type VerifyResult struct {
|
// and options for creating keys.
|
||||||
Endpoint string
|
type CreateKeyRequest struct {
|
||||||
Decrypt string
|
// Name is the name of the key that gets created.
|
||||||
Encrypt string
|
Name string
|
||||||
Version string
|
|
||||||
Status string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status describes the current state of a KMS.
|
// DeleteKeyRequest is a structure containing fields
|
||||||
type Status struct {
|
// and options for deleting keys.
|
||||||
Name string // The name of the KMS
|
type DeleteKeyRequest struct {
|
||||||
Endpoints []string // A set of the KMS endpoints
|
// Name is the name of the key that gets deleted.
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultKey is the key used when no explicit key ID
|
// GenerateKeyRequest is a structure containing fields
|
||||||
// is specified. It is empty if the KMS does not support
|
// and options for generating data keys.
|
||||||
// a default key.
|
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
|
DefaultKey string
|
||||||
|
|
||||||
// Details provides more details about the KMS endpoint status.
|
conn conn // Connection to the KMS
|
||||||
// including uptime, version and available CPUs.
|
|
||||||
// Could be more in future.
|
// Metrics
|
||||||
Details kes.State
|
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
|
// Version returns version information about the KMS.
|
||||||
// plaintext-ciphertext pair and the ID of the key
|
|
||||||
// used to generate the ciphertext.
|
|
||||||
//
|
//
|
||||||
// The plaintext can be used for cryptographic
|
// TODO(aead): refactor this API call since it does not account
|
||||||
// operations - like encrypting some data. The
|
// for multiple KMS/KES servers.
|
||||||
// ciphertext is the encrypted version of the
|
func (k *KMS) Version(ctx context.Context) (string, error) {
|
||||||
// plaintext data and can be stored on untrusted
|
return k.conn.Version(ctx)
|
||||||
// storage.
|
|
||||||
type DEK struct {
|
|
||||||
KeyID string
|
|
||||||
Plaintext []byte
|
|
||||||
Ciphertext []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// APIs returns a list of KMS server APIs.
|
||||||
_ encoding.TextMarshaler = (*DEK)(nil)
|
//
|
||||||
_ encoding.TextUnmarshaler = (*DEK)(nil)
|
// 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
|
// Metrics returns a current snapshot of the KMS metrics.
|
||||||
// as JSON.
|
func (k *KMS) Metrics(ctx context.Context) (*Metrics, error) {
|
||||||
func (d DEK) MarshalText() ([]byte, error) {
|
latency := make(map[time.Duration]uint64, len(k.latencyBuckets))
|
||||||
type JSON struct {
|
for i, b := range k.latencyBuckets {
|
||||||
KeyID string `json:"keyid"`
|
latency[b] = k.latency[i].Load()
|
||||||
Ciphertext []byte `json:"ciphertext"`
|
|
||||||
}
|
}
|
||||||
return json.Marshal(JSON{
|
|
||||||
KeyID: d.KeyID,
|
return &Metrics{
|
||||||
Ciphertext: d.Ciphertext,
|
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
|
func (c *kmsConn) CreateKey(ctx context.Context, req *CreateKeyRequest) error {
|
||||||
// of a DEK and sets DEK's key ID and ciphertext to the
|
if err := c.client.CreateKey(ctx, &kms.CreateKeyRequest{
|
||||||
// decoded values.
|
Enclave: c.enclave,
|
||||||
//
|
Name: req.Name,
|
||||||
// It sets DEK's plaintext to nil.
|
}); err != nil {
|
||||||
func (d *DEK) UnmarshalText(text []byte) error {
|
if errors.Is(err, kms.ErrKeyExists) {
|
||||||
type JSON struct {
|
return ErrKeyExists
|
||||||
KeyID string `json:"keyid"`
|
}
|
||||||
Ciphertext []byte `json:"ciphertext"`
|
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
|
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
|
||||||
|
}
|
||||||
|
@ -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
309
internal/kms/secret-key.go
Normal 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
|
||||||
|
}
|
@ -25,16 +25,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSingleKeyRoundtrip(t *testing.T) {
|
func TestSingleKeyRoundtrip(t *testing.T) {
|
||||||
KMS, err := Parse("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
|
KMS, err := ParseSecretKey("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to initialize KMS: %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to generate key: %v", err)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to decrypt key: %v", err)
|
t.Fatalf("Failed to decrypt key: %v", err)
|
||||||
}
|
}
|
||||||
@ -44,7 +47,7 @@ func TestSingleKeyRoundtrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDecryptKey(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 {
|
if err != nil {
|
||||||
t.Fatalf("Failed to initialize KMS: %v", err)
|
t.Fatalf("Failed to initialize KMS: %v", err)
|
||||||
}
|
}
|
||||||
@ -54,11 +57,11 @@ func TestDecryptKey(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to decode plaintext key: %v", i, err)
|
t.Fatalf("Test %d: failed to decode plaintext key: %v", i, err)
|
||||||
}
|
}
|
||||||
ciphertext, err := base64.StdEncoding.DecodeString(test.Ciphertext)
|
plaintext, err := KMS.Decrypt(context.TODO(), &DecryptRequest{
|
||||||
if err != nil {
|
Name: test.KeyID,
|
||||||
t.Fatalf("Test %d: failed to decode ciphertext key: %v", i, err)
|
Ciphertext: []byte(test.Ciphertext),
|
||||||
}
|
AssociatedData: test.Context,
|
||||||
plaintext, err := KMS.DecryptKey(test.KeyID, ciphertext, test.Context)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test %d: failed to decrypt key: %v", i, err)
|
t.Fatalf("Test %d: failed to decrypt key: %v", i, err)
|
||||||
}
|
}
|
||||||
@ -77,12 +80,12 @@ var decryptKeyTests = []struct {
|
|||||||
{
|
{
|
||||||
KeyID: "my-key",
|
KeyID: "my-key",
|
||||||
Plaintext: "zmS7NrG765UZ0ZN85oPjybelxqVvpz01vxsSpOISy2M=",
|
Plaintext: "zmS7NrG765UZ0ZN85oPjybelxqVvpz01vxsSpOISy2M=",
|
||||||
Ciphertext: "eyJhZWFkIjoiQ2hhQ2hhMjBQb2x5MTMwNSIsIml2IjoiSmJJK3Z3dll3dzFsQ2I1VnBrQUZ1UT09Iiwibm9uY2UiOiJBUmpJakp4QlNENTQxR3o4IiwiYnl0ZXMiOiJLQ2JFYzJzQTBUTHZBN2FXVFdhMjNBZGNjVmZKTXBPeHdnRzhobSs0UGFOcnhZZnkxeEZXWmcyZ0VlblZyT2d2In0=",
|
Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"JbI+vwvYww1lCb5VpkAFuQ==","nonce":"ARjIjJxBSD541Gz8","bytes":"KCbEc2sA0TLvA7aWTWa23AdccVfJMpOxwgG8hm+4PaNrxYfy1xFWZg2gEenVrOgv"}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
KeyID: "my-key",
|
KeyID: "my-key",
|
||||||
Plaintext: "UnPWsZgVI+T4L9WGNzFlP1PsP1Z6hn2Fx8ISeZfDGnA=",
|
Plaintext: "UnPWsZgVI+T4L9WGNzFlP1PsP1Z6hn2Fx8ISeZfDGnA=",
|
||||||
Ciphertext: "eyJhZWFkIjoiQ2hhQ2hhMjBQb2x5MTMwNSIsIml2IjoicjQreWZpVmJWSVlSMFoySTlGcSs2Zz09Iiwibm9uY2UiOiIyWXB3R3dFNTlHY1ZyYUkzIiwiYnl0ZXMiOiJrL3N2TWdsT1U3L0tnd3Y3M2hlRzM4TldXNTc1WExjRnAzU2F4UUhETWpKR1l5UkkzRml5Z3UyT2V1dEdQWE5MIn0=",
|
Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"r4+yfiVbVIYR0Z2I9Fq+6g==","nonce":"2YpwGwE59GcVraI3","bytes":"k/svMglOU7/Kgwv73heG38NWW575XLcFp3SaxQHDMjJGYyRI3Fiygu2OeutGPXNL"}`,
|
||||||
Context: Context{"key": "value"},
|
Context: Context{"key": "value"},
|
||||||
},
|
},
|
||||||
}
|
}
|
@ -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"`
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user