From 8b660e18f26b36cc11a51aad0d806d03b917784d Mon Sep 17 00:00:00 2001 From: Andreas Auernhammer Date: Wed, 8 May 2024 01:55:37 +0200 Subject: [PATCH] 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 --- cmd/admin-bucket-handlers.go | 6 +- cmd/admin-handlers.go | 107 +--- cmd/api-errors.go | 4 +- cmd/batch-rotate.go | 8 +- cmd/bucket-encryption-handlers.go | 2 +- cmd/bucket-metadata.go | 8 +- cmd/common-main.go | 137 +---- cmd/config-current.go | 56 +- cmd/encryption-v1.go | 32 +- cmd/globals.go | 2 +- cmd/healthcheck-handler.go | 2 +- cmd/kms-handlers.go | 451 +------------- cmd/kms-router.go | 15 - cmd/metrics-v2.go | 17 +- cmd/object_api_suite_test.go | 8 +- cmd/site-replication.go | 7 +- go.mod | 1 + go.sum | 2 + internal/config/crypto.go | 16 +- internal/config/crypto_test.go | 4 +- internal/crypto/sse-kms.go | 8 +- internal/crypto/sse-s3.go | 49 +- internal/kms/config.go | 401 +++++++++++- internal/kms/config_test.go | 105 ++++ internal/kms/conn.go | 167 +++++ internal/kms/dek_test.go | 7 + internal/kms/errors.go | 109 +++- internal/kms/identity-manager.go | 39 -- internal/kms/kes.go | 582 +++++------------- internal/kms/key-manager.go | 50 -- internal/kms/kms.go | 479 ++++++++++---- internal/kms/policy-manager.go | 37 -- internal/kms/secret-key.go | 309 ++++++++++ ...{single-key_test.go => secret-key_test.go} | 25 +- internal/kms/single-key.go | 318 ---------- internal/kms/status-manager.go | 32 - 36 files changed, 1794 insertions(+), 1808 deletions(-) create mode 100644 internal/kms/config_test.go create mode 100644 internal/kms/conn.go delete mode 100644 internal/kms/identity-manager.go delete mode 100644 internal/kms/key-manager.go delete mode 100644 internal/kms/policy-manager.go create mode 100644 internal/kms/secret-key.go rename internal/kms/{single-key_test.go => secret-key_test.go} (67%) delete mode 100644 internal/kms/single-key.go delete mode 100644 internal/kms/status-manager.go diff --git a/cmd/admin-bucket-handlers.go b/cmd/admin-bucket-handlers.go index 8a12aa582..1ab5a027f 100644 --- a/cmd/admin-bucket-handlers.go +++ b/cmd/admin-bucket-handlers.go @@ -874,8 +874,10 @@ func (a adminAPIHandlers) ImportBucketMetadataHandler(w http.ResponseWriter, r * } kmsKey := encConfig.KeyID() if kmsKey != "" { - 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: kms.Context{"MinIO admin API": "ServerInfoHandler"}, // Context for a test key operation + }) if err != nil { if errors.Is(err, kes.ErrKeyNotFound) { rpt.SetStatus(bucket, fileName, errKMSKeyNotFound) diff --git a/cmd/admin-handlers.go b/cmd/admin-handlers.go index d8adad9eb..82ba40a83 100644 --- a/cmd/admin-handlers.go +++ b/cmd/admin-handlers.go @@ -2173,7 +2173,9 @@ func (a adminAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Req 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) return } @@ -2194,22 +2196,12 @@ func (a adminAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Reques return } - stat, err := GlobalKMS.Stat(ctx) + stat, err := GlobalKMS.Status(ctx) if err != nil { writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) return } - - 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) + resp, err := json.Marshal(stat) if err != nil { writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) return @@ -2231,15 +2223,9 @@ func (a adminAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Req 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") if keyID == "" { - keyID = stat.DefaultKey + keyID = GlobalKMS.DefaultKey } response := madmin.KMSKeyStatus{ 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 // 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 { response.EncryptionErr = err.Error() 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 - 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 { response.DecryptionErr = err.Error() resp, err := json.Marshal(response) @@ -2413,8 +2406,7 @@ func getServerInfo(ctx context.Context, pools, metrics bool, r *http.Request) ma domain := globalDomainNames services := madmin.Services{ - KMS: fetchKMSStatus(), - KMSStatus: fetchKMSStatusV2(ctx), + KMSStatus: fetchKMSStatus(ctx), LDAP: ldap, Logger: log, Audit: audit, @@ -3024,66 +3016,25 @@ func fetchLambdaInfo() []map[string][]madmin.TargetIDStatus { return notify } -// fetchKMSStatus fetches KMS-related status information. -func fetchKMSStatus() 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 { +// fetchKMSStatus fetches KMS-related status information for all instances +func fetchKMSStatus(ctx context.Context) []madmin.KMS { if GlobalKMS == nil { return []madmin.KMS{} } - results := GlobalKMS.Verify(ctx) - - stats := []madmin.KMS{} - for _, result := range results { - stats = append(stats, madmin.KMS{ - Status: result.Status, - Endpoint: result.Endpoint, - Encrypt: result.Encrypt, - Decrypt: result.Decrypt, - Version: result.Version, - }) + stat, err := GlobalKMS.Status(ctx) + if err != nil { + kmsLogIf(ctx, err, "failed to fetch KMS status information") + return []madmin.KMS{} } + 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 } diff --git a/cmd/api-errors.go b/cmd/api-errors.go index ff6d3a87c..8e7ec4f32 100644 --- a/cmd/api-errors.go +++ b/cmd/api-errors.go @@ -2430,9 +2430,9 @@ func toAPIError(ctx context.Context, err error) APIError { switch e := err.(type) { case kms.Error: apiErr = APIError{ - Description: e.Err.Error(), Code: e.APICode, - HTTPStatusCode: e.HTTPStatusCode, + Description: e.Err, + HTTPStatusCode: e.Code, } case batchReplicationJobError: apiErr = APIError{ diff --git a/cmd/batch-rotate.go b/cmd/batch-rotate.go index 9ba7587ff..c81a899ea 100644 --- a/cmd/batch-rotate.go +++ b/cmd/batch-rotate.go @@ -95,6 +95,7 @@ func (e BatchJobKeyRotateEncryption) Validate() error { if e.Type == ssekms && spaces { return crypto.ErrInvalidEncryptionKeyID } + if e.Type == ssekms && GlobalKMS != nil { ctx := kms.Context{} if e.Context != "" { @@ -113,7 +114,7 @@ func (e BatchJobKeyRotateEncryption) Validate() error { e.kmsContext[k] = v } 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 } } @@ -478,8 +479,5 @@ func (r *BatchJobKeyRotateV1) Validate(ctx context.Context, job BatchJobRequest, } } - if err := r.Flags.Retry.Validate(); err != nil { - return err - } - return nil + return r.Flags.Retry.Validate() } diff --git a/cmd/bucket-encryption-handlers.go b/cmd/bucket-encryption-handlers.go index 018aee6c9..e17edba78 100644 --- a/cmd/bucket-encryption-handlers.go +++ b/cmd/bucket-encryption-handlers.go @@ -85,7 +85,7 @@ func (api objectAPIHandlers) PutBucketEncryptionHandler(w http.ResponseWriter, r kmsKey := encConfig.KeyID() if kmsKey != "" { 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 errors.Is(err, kes.ErrKeyNotFound) { writeErrorResponse(ctx, w, toAPIError(ctx, errKMSKeyNotFound), r.URL) diff --git a/cmd/bucket-metadata.go b/cmd/bucket-metadata.go index 591669022..8f2d72de0 100644 --- a/cmd/bucket-metadata.go +++ b/cmd/bucket-metadata.go @@ -490,7 +490,7 @@ func encryptBucketMetadata(ctx context.Context, bucket string, input []byte, kms } metadata := make(map[string]string) - key, err := GlobalKMS.GenerateKey(ctx, "", kmsContext) + key, err := GlobalKMS.GenerateKey(ctx, &kms.GenerateKeyRequest{AssociatedData: kmsContext}) if err != nil { return } @@ -519,7 +519,11 @@ func decryptBucketMetadata(input []byte, bucket string, meta map[string]string, if err != nil { 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 { return nil, err } diff --git a/cmd/common-main.go b/cmd/common-main.go index 835f861a1..685424115 100644 --- a/cmd/common-main.go +++ b/cmd/common-main.go @@ -21,10 +21,8 @@ import ( "bufio" "bytes" "context" - "crypto/tls" "crypto/x509" "encoding/gob" - "encoding/pem" "errors" "fmt" "net" @@ -49,7 +47,6 @@ import ( "github.com/minio/console/api/operations" consoleoauth2 "github.com/minio/console/pkg/auth/idp/oauth2" consoleCerts "github.com/minio/console/pkg/certs" - "github.com/minio/kms-go/kes" "github.com/minio/madmin-go/v3" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/set" @@ -60,7 +57,6 @@ import ( "github.com/minio/minio/internal/logger" "github.com/minio/pkg/v2/certs" "github.com/minio/pkg/v2/console" - "github.com/minio/pkg/v2/ellipses" "github.com/minio/pkg/v2/env" xnet "github.com/minio/pkg/v2/net" "golang.org/x/term" @@ -865,127 +861,28 @@ func loadRootCredentials() { // Initialize KMS global variable after valiadating and loading the configuration. // It depends on KMS env variables and global cli flags. func handleKMSConfig() { - if env.IsSet(kms.EnvKMSSecretKey) && env.IsSet(kms.EnvKESEndpoint) { - logger.Fatal(errors.New("ambiguous KMS configuration"), fmt.Sprintf("The environment contains %q as well as %q", kms.EnvKMSSecretKey, kms.EnvKESEndpoint)) + present, err := kms.IsPresent() + if err != nil { + logger.Fatal(err, "Invalid KMS configuration specified") + } + if !present { + return } - if env.IsSet(kms.EnvKMSSecretKey) { - KMS, err := kms.Parse(env.Get(kms.EnvKMSSecretKey, "")) - if err != nil { - logger.Fatal(err, "Unable to parse the KMS secret key inherited from the shell environment") - } - GlobalKMS = KMS + KMS, err := kms.Connect(GlobalContext, &kms.ConnectionOptions{ + CADir: globalCertsCADir.Get(), + }) + if err != nil { + logger.Fatal(err, "Failed to connect to 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 - for _, endpoint := range strings.Split(env.Get(kms.EnvKESEndpoint, ""), ",") { - 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 = KMS.GenerateKey(GlobalContext, &kms.GenerateKeyRequest{}); errors.Is(err, kms.ErrKeyNotFound) { + err = KMS.CreateKey(GlobalContext, &kms.CreateKeyRequest{Name: KMS.DefaultKey}) } + 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) { diff --git a/cmd/config-current.go b/cmd/config-current.go index a3f5aee2e..7e55c510d 100644 --- a/cmd/config-current.go +++ b/cmd/config-current.go @@ -753,41 +753,33 @@ func autoGenerateRootCredentials() { return } - if manager, ok := GlobalKMS.(kms.KeyManager); ok { - stat, err := GlobalKMS.Stat(GlobalContext) - if err != nil { - kmsLogIf(GlobalContext, err, "Unable to generate root credentials using KMS") - return - } + aKey, err := GlobalKMS.MAC(GlobalContext, &kms.MACRequest{Message: []byte("root access key")}) + if errors.Is(err, kes.ErrNotAllowed) || errors.Is(err, errors.ErrUnsupported) { + return // If we don't have permission to compute the HMAC, don't change the cred. + } + 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")) - if errors.Is(err, kes.ErrNotAllowed) { - return // If we don't have permission to compute the HMAC, don't change the cred. - } - if err != nil { - logger.Fatal(err, "Unable to generate root access key using KMS") - } + sKey, err := GlobalKMS.MAC(GlobalContext, &kms.MACRequest{Message: []byte("root secret key")}) + if err != nil { + // Here, we must have permission. Otherwise, we would have failed earlier. + logger.Fatal(err, "Unable to generate root secret key using KMS") + } - sKey, err := manager.HMAC(GlobalContext, stat.DefaultKey, []byte("root secret key")) - if err != nil { - // Here, we must have permission. Otherwise, we would have failed earlier. - logger.Fatal(err, "Unable to generate root secret key using KMS") - } + accessKey, err := auth.GenerateAccessKey(20, bytes.NewReader(aKey)) + if err != nil { + logger.Fatal(err, "Unable to generate root access key") + } + 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)) - if err != nil { - logger.Fatal(err, "Unable to generate root access key") - } - 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, - } + logger.Info("Automatically generated root access key and secret key with the KMS") + globalActiveCred = auth.Credentials{ + AccessKey: accessKey, + SecretKey: secretKey, } } diff --git a/cmd/encryption-v1.go b/cmd/encryption-v1.go index e2f062162..c2e70286c 100644 --- a/cmd/encryption-v1.go +++ b/cmd/encryption-v1.go @@ -110,7 +110,7 @@ func kmsKeyIDFromMetadata(metadata map[string]string) string { // // DecryptETags uses a KMS bulk decryption API, if available, which // 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. var ( 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 { 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 { return err } @@ -276,7 +280,10 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt 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 { return err } @@ -312,7 +319,10 @@ func rotateKey(ctx context.Context, oldKey []byte, newKeyID string, newKey []byt if _, ok := kmsCtx[bucket]; !ok { 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 { return err } @@ -352,7 +362,9 @@ func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key if GlobalKMS == nil { 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 { return crypto.ObjectKey{}, err } @@ -379,7 +391,10 @@ func newEncryptMetadata(ctx context.Context, kind crypto.Type, keyID string, key if _, ok := kmsCtx[bucket]; !ok { 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 errors.Is(err, kes.ErrKeyNotFound) { 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) { switch kind, _ := crypto.IsEncrypted(metadata); kind { case crypto.S3: - KMS := GlobalKMS - if KMS == nil { + if GlobalKMS == nil { return nil, errKMSNotConfigured } - objectKey, err := crypto.S3.UnsealObjectKey(KMS, metadata, bucket, object) + objectKey, err := crypto.S3.UnsealObjectKey(GlobalKMS, metadata, bucket, object) if err != nil { return nil, err } diff --git a/cmd/globals.go b/cmd/globals.go index c52d9eb5f..9840741f9 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -349,7 +349,7 @@ var ( globalDNSConfig dns.Store // GlobalKMS initialized KMS configuration - GlobalKMS kms.KMS + GlobalKMS *kms.KMS // Common lock for various subsystems performing the leader tasks globalLeaderLock *sharedLock diff --git a/cmd/healthcheck-handler.go b/cmd/healthcheck-handler.go index 16fa92111..48b14e2ca 100644 --- a/cmd/healthcheck-handler.go +++ b/cmd/healthcheck-handler.go @@ -135,7 +135,7 @@ func ReadinessCheckHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), time.Minute) 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 { case http.MethodHead: apiErr := toAPIError(r.Context(), err) diff --git a/cmd/kms-handlers.go b/cmd/kms-handlers.go index e496b484a..be9ed40bf 100644 --- a/cmd/kms-handlers.go +++ b/cmd/kms-handlers.go @@ -20,10 +20,7 @@ package cmd import ( "crypto/subtle" "encoding/json" - "io" "net/http" - "strings" - "time" "github.com/minio/kms-go/kes" "github.com/minio/madmin-go/v3" @@ -46,22 +43,12 @@ func (a kmsAPIHandlers) KMSStatusHandler(w http.ResponseWriter, r *http.Request) return } - stat, err := GlobalKMS.Stat(ctx) + stat, err := GlobalKMS.Status(ctx) if err != nil { writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) return } - - 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) + resp, err := json.Marshal(stat) if err != nil { writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL) return @@ -84,11 +71,6 @@ func (a kmsAPIHandlers) KMSMetricsHandler(w http.ResponseWriter, r *http.Request return } - if _, ok := GlobalKMS.(kms.KeyManager); !ok { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) - return - } - metrics, err := GlobalKMS.Metrics(ctx) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) @@ -116,13 +98,7 @@ func (a kmsAPIHandlers) KMSAPIsHandler(w http.ResponseWriter, r *http.Request) { return } - manager, ok := GlobalKMS.(kms.StatusManager) - if !ok { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) - return - } - - apis, err := manager.APIs(ctx) + apis, err := GlobalKMS.APIs(ctx) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return @@ -153,13 +129,7 @@ func (a kmsAPIHandlers) KMSVersionHandler(w http.ResponseWriter, r *http.Request return } - manager, ok := GlobalKMS.(kms.StatusManager) - if !ok { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL) - return - } - - version, err := manager.Version(ctx) + version, err := GlobalKMS.Version(ctx) if err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) 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) { // If env variable MINIO_KMS_SECRET_KEY is populated, prevent creation of new keys 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)) objectAPI, _ := validateAdminReq(ctx, w, r, policy.KMSCreateKeyAction) @@ -193,39 +159,7 @@ func (a kmsAPIHandlers) KMSCreateKeyHandler(w http.ResponseWriter, r *http.Reque return } - manager, ok := GlobalKMS.(kms.KeyManager) - 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= -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 { + if err := GlobalKMS.CreateKey(ctx, &kms.CreateKeyRequest{Name: r.Form.Get("key-id")}); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return } @@ -235,15 +169,6 @@ func (a kmsAPIHandlers) KMSDeleteKeyHandler(w http.ResponseWriter, r *http.Reque // KMSListKeysHandler - GET /minio/kms/v1/key/list?pattern= func (a kmsAPIHandlers) KMSListKeysHandler(w http.ResponseWriter, r *http.Request) { 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)) 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) return } - manager, ok := GlobalKMS.(kms.KeyManager) - if !ok { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL) - return - } - keys, err := manager.ListKeys(ctx) + names, _, err := GlobalKMS.ListKeyNames(ctx, &kms.ListRequest{ + Prefix: r.Form.Get("pattern"), + }) 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.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 := make([]kes.KeyInfo, 0, len(names)) + for _, name := range names { values = append(values, kes.KeyInfo{ 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= -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= func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Request) { ctx := newContext(r, w, "KMSKeyStatus") @@ -338,15 +216,9 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque 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") if keyID == "" { - keyID = stat.DefaultKey + keyID = GlobalKMS.DefaultKey } response := madmin.KMSKeyStatus{ 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 // 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 { response.EncryptionErr = err.Error() 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 - 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 { response.DecryptionErr = err.Error() resp, err := json.Marshal(response) @@ -398,296 +274,3 @@ func (a kmsAPIHandlers) KMSKeyStatusHandler(w http.ResponseWriter, r *http.Reque } writeSuccessResponseJSON(w, resp) } - -// KMSDescribePolicyHandler - GET /minio/kms/v1/policy/describe?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= -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= -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= -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= -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= -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= -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= -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) - } -} diff --git a/cmd/kms-router.go b/cmd/kms-router.go index 98c6c55c1..2428f4c1e 100644 --- a/cmd/kms-router.go +++ b/cmd/kms-router.go @@ -57,23 +57,8 @@ func registerKMSRouter(router *mux.Router) { kmsRouter.Methods(http.MethodGet).Path(version + "/version").HandlerFunc(gz(httpTraceAll(kmsAPI.KMSVersionHandler))) // 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/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/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 diff --git a/cmd/metrics-v2.go b/cmd/metrics-v2.go index d307acb13..93ea1f6e9 100644 --- a/cmd/metrics-v2.go +++ b/cmd/metrics-v2.go @@ -3970,7 +3970,7 @@ func getKMSMetrics(opts MetricsGroupOpts) *MetricsGroupV2 { Help: "Number of KMS requests that succeeded", Type: counterMetric, }, - Value: float64(metric.RequestOK), + Value: float64(metric.ReqOK), }) metrics = append(metrics, MetricV2{ 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)", Type: counterMetric, }, - Value: float64(metric.RequestErr), + Value: float64(metric.ReqErr), }) metrics = append(metrics, MetricV2{ 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)", 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 mg diff --git a/cmd/object_api_suite_test.go b/cmd/object_api_suite_test.go index d184ebec0..aa1f4f161 100644 --- a/cmd/object_api_suite_test.go +++ b/cmd/object_api_suite_test.go @@ -521,11 +521,11 @@ func enableCompression(t *testing.T, encrypt bool) { globalCompressConfigMu.Unlock() if encrypt { globalAutoEncryption = encrypt - var err error - GlobalKMS, err = kms.Parse("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=") + KMS, err := kms.ParseSecretKey("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=") if err != nil { t.Fatal(err) } + GlobalKMS = KMS } } @@ -536,11 +536,11 @@ func enableEncryption(t *testing.T) { globalCompressConfigMu.Unlock() globalAutoEncryption = true - var err error - GlobalKMS, err = kms.Parse("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=") + KMS, err := kms.ParseSecretKey("my-minio-key:5lF+0pJM0OWwlQrvK2S/I7W9mO4a6rJJI7wzj7v09cw=") if err != nil { t.Fatal(err) } + GlobalKMS = KMS } func resetCompressEncryption() { diff --git a/cmd/site-replication.go b/cmd/site-replication.go index cc3aeda0f..ffd2925cf 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -856,12 +856,7 @@ func (c *SiteReplicationSys) MakeBucketHook(ctx context.Context, bucket string, if err := errors.Unwrap(makeBucketConcErr); err != nil { return err } - - if err := errors.Unwrap(makeRemotesConcErr); err != nil { - return err - } - - return nil + return errors.Unwrap(makeRemotesConcErr) } // DeleteBucketHook - called during a regular delete bucket call when cluster diff --git a/go.mod b/go.mod index c14c3ce5f..f76de0e60 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,7 @@ require ( github.com/minio/dperf v0.5.3 github.com/minio/highwayhash v1.0.2 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/minio-go/v7 v7.0.70 github.com/minio/mux v1.9.0 diff --git a/go.sum b/go.sum index 801ad8383..b32991a6f 100644 --- a/go.sum +++ b/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/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/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/go.mod h1:IFAwr0XMrdsLovxAdCcuq/eoL4nRuMVQQv0iubJANQw= github.com/minio/mc v0.0.0-20240430174448-dcb911bed9d5 h1:VDXLzvY0Jxk4lzIntGXZuw0VH7S1JgQBmjWGkz7xphU= diff --git a/internal/config/crypto.go b/internal/config/crypto.go index a3b80dc30..ecacdbca5 100644 --- a/internal/config/crypto.go +++ b/internal/config/crypto.go @@ -38,7 +38,7 @@ import ( // // The same context must be provided when decrypting the // 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) if err != nil { 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. // The same context that have been used during encryption must be // 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) if err != nil { 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 // 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 if !fips.Enabled && !sioutil.NativeAES() { algorithm = sio.ChaCha20Poly1305 } - key, err := k.GenerateKey(context.Background(), "", ctx) + key, err := k.GenerateKey(context.Background(), &kms.GenerateKeyRequest{AssociatedData: ctx}) if err != nil { 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. // The same context that have been used during encryption must be // 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 ( MaxMetadataSize = 1 << 20 // max. size of the metadata 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) } - 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 { return nil, err } diff --git a/internal/config/crypto_test.go b/internal/config/crypto_test.go index 1573e587f..75dbe9a6b 100644 --- a/internal/config/crypto_test.go +++ b/internal/config/crypto_test.go @@ -53,7 +53,7 @@ func TestEncryptDecrypt(t *testing.T) { if err != nil { 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 { t.Fatalf("Failed to create KMS: %v", err) } @@ -88,7 +88,7 @@ func BenchmarkEncrypt(b *testing.B) { if err != nil { 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 { b.Fatalf("Failed to create KMS: %v", err) } diff --git a/internal/crypto/sse-kms.go b/internal/crypto/sse-kms.go index 594262546..dd0aa46a2 100644 --- a/internal/crypto/sse-kms.go +++ b/internal/crypto/sse-kms.go @@ -106,7 +106,7 @@ func (ssekms) IsEncrypted(metadata map[string]string) bool { // UnsealObjectKey extracts and decrypts the sealed object key // from the metadata using KMS and returns the decrypted object // 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 { 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 { 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 { return key, err } diff --git a/internal/crypto/sse-s3.go b/internal/crypto/sse-s3.go index bda3449c0..ce34d5a4f 100644 --- a/internal/crypto/sse-s3.go +++ b/internal/crypto/sse-s3.go @@ -71,7 +71,7 @@ func (sses3) IsEncrypted(metadata map[string]string) bool { // UnsealObjectKey extracts and decrypts the sealed object key // from the metadata using KMS and returns the decrypted object // 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 { 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 { 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 { return key, err } @@ -92,7 +96,7 @@ func (s3 sses3) UnsealObjectKey(k kms.KMS, metadata map[string]string, bucket, o // keys. // // 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 { 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) { return nil, Errorf("invalid metadata/object count: %d != %d != %d", len(metadata), len(buckets), len(objects)) } - - keyIDs := make([]string, 0, len(metadata)) - kmsKeys := make([][]byte, 0, len(metadata)) - sealedKeys := make([]SealedKey, 0, len(metadata)) - - sameKeyID := true + keys := make([]ObjectKey, 0, len(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]) if err != nil { return nil, err diff --git a/internal/kms/config.go b/internal/kms/config.go index ba38aea9a..6390dd593 100644 --- a/internal/kms/config.go +++ b/internal/kms/config.go @@ -17,16 +17,393 @@ package kms -// Top level config constants for KMS -const ( - EnvKMSSecretKey = "MINIO_KMS_SECRET_KEY" - EnvKMSSecretKeyFile = "MINIO_KMS_SECRET_KEY_FILE" - EnvKESEndpoint = "MINIO_KMS_KES_ENDPOINT" // One or multiple KES endpoints, separated by ',' - EnvKESKeyName = "MINIO_KMS_KES_KEY_NAME" // The default key name used for IAM data and when no key ID is specified on a bucket - EnvKESAPIKey = "MINIO_KMS_KES_API_KEY" // Access credential for KES - API keys and private key / certificate are mutually exclusive - EnvKESClientKey = "MINIO_KMS_KES_KEY_FILE" // Path to TLS private key for authenticating to KES with mTLS - usually prefer API keys - EnvKESClientPassword = "MINIO_KMS_KES_KEY_PASSWORD" // Optional password to decrypt an encrypt TLS private key - EnvKESClientCert = "MINIO_KMS_KES_CERT_FILE" // Path to TLS certificate for authenticating to KES with mTLS - usually prefer API keys - EnvKESServerCA = "MINIO_KMS_KES_CAPATH" // Path to file/directory containing CA certificates to verify the KES server certificate - EnvKESKeyCacheInterval = "MINIO_KMS_KEY_CACHE_INTERVAL" // Period between polls of the KES KMS Master Key cache, to prevent it from being unused and purged +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "os" + "strings" + "sync/atomic" + "syscall" + "time" + + "github.com/minio/kms-go/kes" + "github.com/minio/kms-go/kms" + "github.com/minio/pkg/v2/certs" + "github.com/minio/pkg/v2/ellipses" + "github.com/minio/pkg/v2/env" ) + +// Environment variables for MinIO KMS. +const ( + EnvKMSEndpoint = "MINIO_KMS_SERVER" // List of MinIO KMS endpoints, separated by ',' + EnvKMSEnclave = "MINIO_KMS_ENCLAVE" // MinIO KMS enclave in which the key and identity exists + EnvKMSDefaultKey = "MINIO_KMS_SSE_KEY" // Default key used for SSE-S3 or when no SSE-KMS key ID is specified + EnvKMSAPIKey = "MINIO_KMS_API_KEY" // Credential to access the MinIO KMS. +) + +// Environment variables for MinIO KES. +const ( + EnvKESEndpoint = "MINIO_KMS_KES_ENDPOINT" // One or multiple KES endpoints, separated by ',' + EnvKESDefaultKey = "MINIO_KMS_KES_KEY_NAME" // The default key name used for IAM data and when no key ID is specified on a bucket + EnvKESAPIKey = "MINIO_KMS_KES_API_KEY" // Access credential for KES - API keys and private key / certificate are mutually exclusive + EnvKESClientKey = "MINIO_KMS_KES_KEY_FILE" // Path to TLS private key for authenticating to KES with mTLS - usually prefer API keys + EnvKESClientCert = "MINIO_KMS_KES_CERT_FILE" // Path to TLS certificate for authenticating to KES with mTLS - usually prefer API keys + EnvKESServerCA = "MINIO_KMS_KES_CAPATH" // Path to file/directory containing CA certificates to verify the KES server certificate + EnvKESClientPassword = "MINIO_KMS_KES_KEY_PASSWORD" // Optional password to decrypt an encrypt TLS private key +) + +// Environment variables for static KMS key. +const ( + EnvKMSSecretKey = "MINIO_KMS_SECRET_KEY" // Static KMS key in the form ":". 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 +} diff --git a/internal/kms/config_test.go b/internal/kms/config_test.go new file mode 100644 index 000000000..65e720273 --- /dev/null +++ b/internal/kms/config_test.go @@ -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 . + +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, + }, +} diff --git a/internal/kms/conn.go b/internal/kms/conn.go new file mode 100644 index 000000000..ecdc30201 --- /dev/null +++ b/internal/kms/conn.go @@ -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 . + +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 +} diff --git a/internal/kms/dek_test.go b/internal/kms/dek_test.go index dd83eca6e..12ab164d5 100644 --- a/internal/kms/dek_test.go +++ b/internal/kms/dek_test.go @@ -41,6 +41,13 @@ var dekEncodeDecodeTests = []struct { Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="), }, }, + { + Key: DEK{ + Version: 3, + Plaintext: mustDecodeB64("GM2UvLXp/X8lzqq0mibFC0LayDCGlmTHQhYLj7qAy7Q="), + Ciphertext: mustDecodeB64("eyJhZWFkIjoiQUVTLTI1Ni1HQ00tSE1BQy1TSEEtMjU2IiwiaXYiOiJ3NmhLUFVNZXVtejZ5UlVZL29pTFVBPT0iLCJub25jZSI6IktMSEU3UE1jRGo2N2UweHkiLCJieXRlcyI6Ik1wUkhjQWJaTzZ1Sm5lUGJGcnpKTkxZOG9pdkxwTmlUcTNLZ0hWdWNGYkR2Y0RlbEh1c1lYT29zblJWVTZoSXIifQ=="), + }, + }, } func TestEncodeDecodeDEK(t *testing.T) { diff --git a/internal/kms/errors.go b/internal/kms/errors.go index 7900c0a23..4f7b87d6a 100644 --- a/internal/kms/errors.go +++ b/internal/kms/errors.go @@ -17,13 +17,112 @@ package kms -// Error encapsulates S3 API error response fields. +import ( + "fmt" + "net/http" +) + +var ( + // ErrPermission is an error returned by the KMS when it has not + // enough permissions to perform the operation. + ErrPermission = Error{ + Code: http.StatusForbidden, + APICode: "kms:NotAuthorized", + Err: "insufficient permissions to perform KMS operation", + } + + // ErrKeyExists is an error returned by the KMS when trying to + // create a key that already exists. + ErrKeyExists = Error{ + Code: http.StatusConflict, + APICode: "kms:KeyAlreadyExists", + Err: "key with given key ID already exits", + } + + // ErrKeyNotFound is an error returned by the KMS when trying to + // use a key that does not exist. + ErrKeyNotFound = Error{ + Code: http.StatusNotFound, + APICode: "kms:KeyNotFound", + Err: "key with given key ID does not exit", + } + + // ErrDecrypt is an error returned by the KMS when the decryption + // of a ciphertext failed. + ErrDecrypt = Error{ + Code: http.StatusBadRequest, + APICode: "kms:InvalidCiphertextException", + Err: "failed to decrypt ciphertext", + } + + // ErrNotSupported is an error returned by the KMS when the requested + // functionality is not supported by the KMS service. + ErrNotSupported = Error{ + Code: http.StatusNotImplemented, + APICode: "kms:NotSupported", + Err: "requested functionality is not supported", + } +) + +// Error is a KMS error that can be translated into an S3 API error. +// +// It does not implement the standard error Unwrap interface for +// better error log messages. type Error struct { - Err error - APICode string - HTTPStatusCode int + Code int // The HTTP status code returned to the client + APICode string // The API error code identifying the error + Err string // The error message returned to the client + Cause error // Optional, lower level error cause. } func (e Error) Error() string { - return e.Err.Error() + if e.Cause == nil { + return e.Err + } + return fmt.Sprintf("%s: %v", e.Err, e.Cause) +} + +func errKeyCreationFailed(err error) Error { + return Error{ + Code: http.StatusInternalServerError, + APICode: "kms:KeyCreationFailed", + Err: "failed to create KMS key", + Cause: err, + } +} + +func errKeyDeletionFailed(err error) Error { + return Error{ + Code: http.StatusInternalServerError, + APICode: "kms:KeyDeletionFailed", + Err: "failed to delete KMS key", + Cause: err, + } +} + +func errListingKeysFailed(err error) Error { + return Error{ + Code: http.StatusInternalServerError, + APICode: "kms:KeyListingFailed", + Err: "failed to list keys at the KMS", + Cause: err, + } +} + +func errKeyGenerationFailed(err error) Error { + return Error{ + Code: http.StatusInternalServerError, + APICode: "kms:KeyGenerationFailed", + Err: "failed to generate data key with KMS key", + Cause: err, + } +} + +func errDecryptionFailed(err error) Error { + return Error{ + Code: http.StatusInternalServerError, + APICode: "kms:DecryptionFailed", + Err: "failed to decrypt ciphertext with KMS key", + Cause: err, + } } diff --git a/internal/kms/identity-manager.go b/internal/kms/identity-manager.go deleted file mode 100644 index ad3a1e8ae..000000000 --- a/internal/kms/identity-manager.go +++ /dev/null @@ -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 . - -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) -} diff --git a/internal/kms/kes.go b/internal/kms/kes.go index bce1b123c..b9df696cf 100644 --- a/internal/kms/kes.go +++ b/internal/kms/kes.go @@ -18,239 +18,116 @@ package kms import ( - "bytes" "context" - "crypto/subtle" - "crypto/tls" - "crypto/x509" "errors" - "fmt" - "strings" + "net/http" "sync" "time" - "github.com/minio/pkg/v2/env" - "github.com/minio/kms-go/kes" - "github.com/minio/pkg/v2/certs" + "github.com/minio/madmin-go/v3" ) -const ( - tlsClientSessionCacheSize = 100 -) - -// Config contains various KMS-related configuration -// parameters - like KMS endpoints or authentication -// credentials. -type Config struct { - // Endpoints contains a list of KMS server - // HTTP endpoints. - Endpoints []string - - // DefaultKeyID is the key ID used when - // no explicit key ID is specified for - // a cryptographic operation. - DefaultKeyID string - - // APIKey is an credential provided by env. var. - // to authenticate to a KES server. Either an - // API key or a client certificate must be specified. - APIKey kes.APIKey - - // Certificate is the client TLS certificate - // to authenticate to KMS via mTLS. - Certificate *certs.Certificate - - // ReloadCertEvents is an event channel that receives - // the reloaded client certificate. - ReloadCertEvents <-chan tls.Certificate - - // RootCAs is a set of root CA certificates - // to verify the KMS server TLS certificate. - RootCAs *x509.CertPool -} - -// NewWithConfig returns a new KMS using the given -// configuration. -func NewWithConfig(config Config, logger Logger) (KMS, error) { - if len(config.Endpoints) == 0 { - return nil, errors.New("kms: no server endpoints") - } - endpoints := make([]string, len(config.Endpoints)) // Copy => avoid being affect by any changes to the original slice - copy(endpoints, config.Endpoints) - - var client *kes.Client - if config.APIKey != nil { - cert, err := kes.GenerateCertificate(config.APIKey) - if err != nil { - return nil, err - } - client = kes.NewClientWithConfig("", &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{cert}, - RootCAs: config.RootCAs, - ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize), - }) - } else { - client = kes.NewClientWithConfig("", &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{config.Certificate.Get()}, - RootCAs: config.RootCAs, - ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize), - }) - } - client.Endpoints = endpoints - - c := &kesClient{ - client: client, - defaultKeyID: config.DefaultKeyID, - } - go func() { - if config.Certificate == nil || config.ReloadCertEvents == nil { - return - } - var prevCertificate tls.Certificate - for { - certificate, ok := <-config.ReloadCertEvents - if !ok { - return - } - sameCert := len(certificate.Certificate) == len(prevCertificate.Certificate) - for i, b := range certificate.Certificate { - if !sameCert { - break - } - sameCert = sameCert && bytes.Equal(b, prevCertificate.Certificate[i]) - } - // Do not reload if its the same cert as before. - if !sameCert { - client := kes.NewClientWithConfig("", &tls.Config{ - MinVersion: tls.VersionTLS12, - Certificates: []tls.Certificate{certificate}, - RootCAs: config.RootCAs, - ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize), - }) - client.Endpoints = endpoints - - c.lock.Lock() - c.client = client - c.lock.Unlock() - - prevCertificate = certificate - } - } - }() - - go c.refreshKMSMasterKeyCache(logger) - return c, nil -} - -// Request KES keep an up-to-date copy of the KMS master key to allow minio to start up even if KMS is down. The -// cached key may still be evicted if the period of this function is longer than that of KES .cache.expiry.unused -func (c *kesClient) refreshKMSMasterKeyCache(logger Logger) { - ctx := context.Background() - - defaultCacheDuration := 10 * time.Second - cacheDuration, err := env.GetDuration(EnvKESKeyCacheInterval, defaultCacheDuration) - if err != nil { - logger.LogOnceIf(ctx, fmt.Errorf("%s, using default of 10s", err.Error()), "refresh-kms-master-key") - cacheDuration = defaultCacheDuration - } - if cacheDuration < time.Second { - logger.LogOnceIf(ctx, errors.New("cache duration is less than 1s, using default of 10s"), "refresh-kms-master-key") - cacheDuration = defaultCacheDuration - } - timer := time.NewTimer(cacheDuration) - defer timer.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-timer.C: - c.RefreshKey(ctx, logger) - - // Reset for the next interval - timer.Reset(cacheDuration) - } - } -} - -type kesClient struct { - lock sync.RWMutex +type kesConn struct { defaultKeyID string client *kes.Client } -var ( // compiler checks - _ KMS = (*kesClient)(nil) - _ KeyManager = (*kesClient)(nil) - _ IdentityManager = (*kesClient)(nil) - _ PolicyManager = (*kesClient)(nil) -) - -// Stat returns the current KES status containing a -// list of KES endpoints and the default key ID. -func (c *kesClient) Stat(ctx context.Context) (Status, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - st, err := c.client.Status(ctx) - if err != nil { - return Status{}, err - } - endpoints := make([]string, len(c.client.Endpoints)) - copy(endpoints, c.client.Endpoints) - return Status{ - Name: "KES", - Endpoints: endpoints, - DefaultKey: c.defaultKeyID, - Details: st, - }, nil -} - -// IsLocal returns true if the KMS is a local implementation -func (c *kesClient) IsLocal() bool { - return env.IsSet(EnvKMSSecretKey) -} - -// List returns an array of local KMS Names -func (c *kesClient) List() []kes.KeyInfo { - var kmsSecret []kes.KeyInfo - envKMSSecretKey := env.Get(EnvKMSSecretKey, "") - values := strings.SplitN(envKMSSecretKey, ":", 2) - if len(values) == 2 { - kmsSecret = []kes.KeyInfo{ - { - Name: values[0], - }, - } - } - return kmsSecret -} - -// Metrics retrieves server metrics in the Prometheus exposition format. -func (c *kesClient) Metrics(ctx context.Context) (kes.Metric, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.Metrics(ctx) -} - -// Version retrieves version information -func (c *kesClient) Version(ctx context.Context) (string, error) { - c.lock.RLock() - defer c.lock.RUnlock() - +func (c *kesConn) Version(ctx context.Context) (string, error) { return c.client.Version(ctx) } -// APIs retrieves a list of supported API endpoints -func (c *kesClient) APIs(ctx context.Context) ([]kes.API, error) { - c.lock.RLock() - defer c.lock.RUnlock() +func (c *kesConn) APIs(ctx context.Context) ([]madmin.KMSAPI, error) { + APIs, err := c.client.APIs(ctx) + if err != nil { + if errors.Is(err, kes.ErrNotAllowed) { + return nil, ErrPermission + } + return nil, Error{ + Code: http.StatusInternalServerError, + APICode: "kms:InternalError", + Err: "failed to list KMS APIs", + Cause: err, + } + } - return c.client.APIs(ctx) + list := make([]madmin.KMSAPI, 0, len(APIs)) + for _, api := range APIs { + list = append(list, madmin.KMSAPI{ + Method: api.Method, + Path: api.Path, + MaxBody: api.MaxBody, + Timeout: int64(api.Timeout.Truncate(time.Second).Seconds()), + }) + } + return list, nil +} + +// Stat returns the current KES status containing a +// list of KES endpoints and the default key ID. +func (c *kesConn) Status(ctx context.Context) (map[string]madmin.ItemState, error) { + if len(c.client.Endpoints) == 1 { + if _, err := c.client.Status(ctx); err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil, err + } + if errors.Is(err, kes.ErrNotAllowed) { + return nil, ErrPermission + } + + return map[string]madmin.ItemState{ + c.client.Endpoints[0]: madmin.ItemOffline, + }, nil + } + return map[string]madmin.ItemState{ + c.client.Endpoints[0]: madmin.ItemOnline, + }, nil + } + + type Result struct { + Endpoint string + ItemState madmin.ItemState + } + + var wg sync.WaitGroup + results := make([]Result, len(c.client.Endpoints)) + for i := range c.client.Endpoints { + wg.Add(1) + go func(i int) { + defer wg.Done() + + client := kes.Client{ + Endpoints: []string{c.client.Endpoints[i]}, + HTTPClient: c.client.HTTPClient, + } + + var item madmin.ItemState + if _, err := client.Status(ctx); err == nil { + item = madmin.ItemOnline + } else { + item = madmin.ItemOffline + } + results[i] = Result{ + Endpoint: c.client.Endpoints[i], + ItemState: item, + } + }(i) + } + wg.Wait() + + status := make(map[string]madmin.ItemState, len(results)) + for _, r := range results { + if r.ItemState == madmin.ItemOnline { + status[r.Endpoint] = madmin.ItemOnline + } else { + status[r.Endpoint] = madmin.ItemOffline + } + } + return status, nil +} + +func (c *kesConn) ListKeyNames(ctx context.Context, req *ListRequest) ([]string, string, error) { + return c.client.ListKeys(ctx, req.Prefix, req.Limit) } // CreateKey tries to create a new key at the KMS with the @@ -258,32 +135,34 @@ func (c *kesClient) APIs(ctx context.Context) ([]kes.API, error) { // // If the a key with the same keyID already exists then // CreateKey returns kes.ErrKeyExists. -func (c *kesClient) CreateKey(ctx context.Context, keyID string) error { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.CreateKey(ctx, keyID) +func (c *kesConn) CreateKey(ctx context.Context, req *CreateKeyRequest) error { + if err := c.client.CreateKey(ctx, req.Name); err != nil { + if errors.Is(err, kes.ErrKeyExists) { + return ErrKeyExists + } + if errors.Is(err, kes.ErrNotAllowed) { + return ErrPermission + } + return errKeyCreationFailed(err) + } + return nil } // DeleteKey deletes a key at the KMS with the given key ID. // Please note that is a dangerous operation. // Once a key has been deleted all data that has been encrypted with it cannot be decrypted // anymore, and therefore, is lost. -func (c *kesClient) DeleteKey(ctx context.Context, keyID string) error { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.DeleteKey(ctx, keyID) -} - -// ListKeys returns an iterator over all key names. -func (c *kesClient) ListKeys(ctx context.Context) (*kes.ListIter[string], error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return &kes.ListIter[string]{ - NextFunc: c.client.ListKeys, - }, nil +func (c *kesConn) DeleteKey(ctx context.Context, req *DeleteKeyRequest) error { + if err := c.client.DeleteKey(ctx, req.Name); err != nil { + if errors.Is(err, kes.ErrKeyNotFound) { + return ErrKeyNotFound + } + if errors.Is(err, kes.ErrNotAllowed) { + return ErrPermission + } + return errKeyDeletionFailed(err) + } + return nil } // GenerateKey generates a new data encryption key using @@ -294,34 +173,36 @@ func (c *kesClient) ListKeys(ctx context.Context) (*kes.ListIter[string], error) // The context is associated and tied to the generated DEK. // The same context must be provided when the generated // key should be decrypted. -func (c *kesClient) GenerateKey(ctx context.Context, keyID string, cryptoCtx Context) (DEK, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - if keyID == "" { - keyID = c.defaultKeyID - } - ctxBytes, err := cryptoCtx.MarshalText() +func (c *kesConn) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) { + aad, err := req.AssociatedData.MarshalText() if err != nil { return DEK{}, err } - dek, err := c.client.GenerateKey(ctx, keyID, ctxBytes) + name := req.Name + if name == "" { + name = c.defaultKeyID + } + + dek, err := c.client.GenerateKey(ctx, name, aad) if err != nil { - return DEK{}, err + if errors.Is(err, kes.ErrKeyNotFound) { + return DEK{}, ErrKeyNotFound + } + if errors.Is(err, kes.ErrNotAllowed) { + return DEK{}, ErrPermission + } + return DEK{}, errKeyGenerationFailed(err) } return DEK{ - KeyID: keyID, + KeyID: name, Plaintext: dek.Plaintext, Ciphertext: dek.Ciphertext, }, nil } // ImportKey imports a cryptographic key into the KMS. -func (c *kesClient) ImportKey(ctx context.Context, keyID string, bytes []byte) error { - c.lock.RLock() - defer c.lock.RUnlock() - +func (c *kesConn) ImportKey(ctx context.Context, keyID string, bytes []byte) error { return c.client.ImportKey(ctx, keyID, &kes.ImportKeyRequest{ Key: bytes, }) @@ -329,10 +210,7 @@ func (c *kesClient) ImportKey(ctx context.Context, keyID string, bytes []byte) e // EncryptKey Encrypts and authenticates a (small) plaintext with the cryptographic key // The plaintext must not exceed 1 MB -func (c *kesClient) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) { - c.lock.RLock() - defer c.lock.RUnlock() - +func (c *kesConn) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) { ctxBytes, err := ctx.MarshalText() if err != nil { return nil, err @@ -343,184 +221,42 @@ func (c *kesClient) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]b // DecryptKey decrypts the ciphertext with the key at the KES // server referenced by the key ID. The context must match the // context value used to generate the ciphertext. -func (c *kesClient) DecryptKey(keyID string, ciphertext []byte, ctx Context) ([]byte, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - ctxBytes, err := ctx.MarshalText() +func (c *kesConn) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) { + aad, err := req.AssociatedData.MarshalText() if err != nil { return nil, err } - return c.client.Decrypt(context.Background(), keyID, ciphertext, ctxBytes) -} -func (c *kesClient) DecryptAll(ctx context.Context, keyID string, ciphertexts [][]byte, contexts []Context) ([][]byte, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - plaintexts := make([][]byte, 0, len(ciphertexts)) - for i := range ciphertexts { - ctxBytes, err := contexts[i].MarshalText() - if err != nil { - return nil, err + plaintext, err := c.client.Decrypt(context.Background(), req.Name, req.Ciphertext, aad) + if err != nil { + if errors.Is(err, kes.ErrKeyNotFound) { + return nil, ErrKeyNotFound } - plaintext, err := c.client.Decrypt(ctx, keyID, ciphertexts[i], ctxBytes) - if err != nil { - return nil, err + if errors.Is(err, kes.ErrDecrypt) { + return nil, ErrDecrypt } - plaintexts = append(plaintexts, plaintext) + if errors.Is(err, kes.ErrNotAllowed) { + return nil, ErrPermission + } + return nil, errDecryptionFailed(err) } - return plaintexts, nil + return plaintext, nil } -// HMAC generates the HMAC checksum of the given msg using the key -// with the given keyID at the KMS. -func (c *kesClient) HMAC(ctx context.Context, keyID string, msg []byte) ([]byte, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.HMAC(context.Background(), keyID, msg) -} - -// DescribePolicy describes a policy by returning its metadata. -// e.g. who created the policy at which point in time. -func (c *kesClient) DescribePolicy(ctx context.Context, policy string) (*kes.PolicyInfo, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.DescribePolicy(ctx, policy) -} - -// ListPolicies returns an iterator over all policy names. -func (c *kesClient) ListPolicies(ctx context.Context) (*kes.ListIter[string], error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return &kes.ListIter[string]{ - NextFunc: c.client.ListPolicies, - }, nil -} - -// GetPolicy gets a policy from KMS. -func (c *kesClient) GetPolicy(ctx context.Context, policy string) (*kes.Policy, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.GetPolicy(ctx, policy) -} - -// DescribeIdentity describes an identity by returning its metadata. -// e.g. which policy is currently assigned and whether its an admin identity. -func (c *kesClient) DescribeIdentity(ctx context.Context, identity string) (*kes.IdentityInfo, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.DescribeIdentity(ctx, kes.Identity(identity)) -} - -// DescribeSelfIdentity describes the identity issuing the request. -// It infers the identity from the TLS client certificate used to authenticate. -// It returns the identity and policy information for the client identity. -func (c *kesClient) DescribeSelfIdentity(ctx context.Context) (*kes.IdentityInfo, *kes.Policy, error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return c.client.DescribeSelf(ctx) -} - -// ListIdentities returns an iterator over all identities. -func (c *kesClient) ListIdentities(ctx context.Context) (*kes.ListIter[kes.Identity], error) { - c.lock.RLock() - defer c.lock.RUnlock() - - return &kes.ListIter[kes.Identity]{ - NextFunc: c.client.ListIdentities, - }, nil -} - -// Verify verifies all KMS endpoints and returns details -func (c *kesClient) Verify(ctx context.Context) []VerifyResult { - c.lock.RLock() - defer c.lock.RUnlock() - - results := []VerifyResult{} - kmsContext := Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation - for _, endpoint := range c.client.Endpoints { - client := kes.Client{ - Endpoints: []string{endpoint}, - HTTPClient: c.client.HTTPClient, +// MAC generates the checksum of the given req.Message using the key +// with the req.Name at the KMS. +func (c *kesConn) MAC(ctx context.Context, req *MACRequest) ([]byte, error) { + mac, err := c.client.HMAC(context.Background(), req.Name, req.Message) + if err != nil { + if errors.Is(err, kes.ErrKeyNotFound) { + return nil, ErrKeyNotFound } - - // 1. Get stats for the KES instance - state, err := client.Status(ctx) - if err != nil { - results = append(results, VerifyResult{Status: "offline", Endpoint: endpoint}) - continue + if errors.Is(err, kes.ErrNotAllowed) { + return nil, ErrPermission } - - // 2. Generate a new key using the KMS. - kmsCtx, err := kmsContext.MarshalText() - if err != nil { - results = append(results, VerifyResult{Status: "offline", Endpoint: endpoint}) - continue - } - result := VerifyResult{Status: "online", Endpoint: endpoint, Version: state.Version} - key, err := client.GenerateKey(ctx, env.Get(EnvKESKeyName, ""), kmsCtx) - if err != nil { - result.Encrypt = fmt.Sprintf("Encryption failed: %v", err) - } else { - result.Encrypt = "success" - } - // 3. Verify that we can indeed decrypt the (encrypted) key - decryptedKey, err := client.Decrypt(ctx, env.Get(EnvKESKeyName, ""), key.Ciphertext, kmsCtx) - switch { - case err != nil: - result.Decrypt = fmt.Sprintf("Decryption failed: %v", err) - case subtle.ConstantTimeCompare(key.Plaintext, decryptedKey) != 1: - result.Decrypt = "Decryption failed: decrypted key does not match generated key" - default: - result.Decrypt = "success" - } - results = append(results, result) - } - return results -} - -// Logger interface permits access to module specific logging, in this case, for KMS -type Logger interface { - LogOnceIf(ctx context.Context, err error, id string, errKind ...interface{}) - LogIf(ctx context.Context, err error, errKind ...interface{}) -} - -// RefreshKey checks the validity of the KMS Master Key -func (c *kesClient) RefreshKey(ctx context.Context, logger Logger) bool { - c.lock.RLock() - defer c.lock.RUnlock() - - validKey := false - kmsContext := Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation - for _, endpoint := range c.client.Endpoints { - client := kes.Client{ - Endpoints: []string{endpoint}, - HTTPClient: c.client.HTTPClient, - } - - // 1. Generate a new key using the KMS. - kmsCtx, err := kmsContext.MarshalText() - if err != nil { - logger.LogOnceIf(ctx, err, "refresh-kms-master-key") - validKey = false - break - } - _, err = client.GenerateKey(ctx, env.Get(EnvKESKeyName, ""), kmsCtx) - if err != nil { - logger.LogOnceIf(ctx, err, "refresh-kms-master-key") - validKey = false - break - } - if !validKey { - validKey = true + if kErr, ok := err.(kes.Error); ok && kErr.Status() == http.StatusNotImplemented { + return nil, ErrNotSupported } } - return validKey + return mac, nil } diff --git a/internal/kms/key-manager.go b/internal/kms/key-manager.go deleted file mode 100644 index 414272d50..000000000 --- a/internal/kms/key-manager.go +++ /dev/null @@ -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 . - -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) -} diff --git a/internal/kms/kms.go b/internal/kms/kms.go index 04bfee7ba..05ba0741a 100644 --- a/internal/kms/kms.go +++ b/internal/kms/kms.go @@ -19,132 +19,403 @@ package kms import ( "context" - "encoding" - "encoding/json" + "errors" + "net/http" + "slices" + "sync/atomic" + "time" - jsoniter "github.com/json-iterator/go" - "github.com/minio/kms-go/kes" + "github.com/minio/kms-go/kms" + "github.com/minio/madmin-go/v3" ) -// KMS is the generic interface that abstracts over -// different KMS implementations. -type KMS interface { - // Stat returns the current KMS status. - Stat(cxt context.Context) (Status, error) +// ListRequest is a structure containing fields +// and options for listing keys. +type ListRequest struct { + // Prefix is an optional prefix for filtering names. + // A list operation only returns elements that match + // this prefix. + // An empty prefix matches any value. + Prefix string - // IsLocal returns true if the KMS is a local implementation - IsLocal() bool + // ContinueAt is the name of the element from where + // a listing should continue. It allows paginated + // listings. + ContinueAt string - // List returns an array of local KMS Names - List() []kes.KeyInfo - - // Metrics returns a KMS metric snapshot. - Metrics(ctx context.Context) (kes.Metric, error) - - // CreateKey creates a new key at the KMS with the given key ID. - CreateKey(ctx context.Context, keyID string) error - - // GenerateKey generates a new data encryption key using the - // key referenced by the key ID. - // - // The KMS may use a default key if the key ID is empty. - // GenerateKey returns an error if the referenced key does - // not exist. - // - // The context is associated and tied to the generated DEK. - // The same context must be provided when the generated key - // should be decrypted. Therefore, it is the callers - // responsibility to remember the corresponding context for - // a particular DEK. The context may be nil. - GenerateKey(ctx context.Context, keyID string, context Context) (DEK, error) - - // DecryptKey decrypts the ciphertext with the key referenced - // by the key ID. The context must match the context value - // used to generate the ciphertext. - DecryptKey(keyID string, ciphertext []byte, context Context) ([]byte, error) - - // DecryptAll decrypts all ciphertexts with the key referenced - // by the key ID. The contexts must match the context value - // used to generate the ciphertexts. - DecryptAll(ctx context.Context, keyID string, ciphertext [][]byte, context []Context) ([][]byte, error) - - // Verify verifies all KMS endpoints and returns the details - Verify(cxt context.Context) []VerifyResult + // Limit limits the number of elements returned by + // a single list operation. If <= 0, a reasonable + // limit is selected automatically. + Limit int } -// VerifyResult describes the verification result details a KMS endpoint -type VerifyResult struct { - Endpoint string - Decrypt string - Encrypt string - Version string - Status string +// CreateKeyRequest is a structure containing fields +// and options for creating keys. +type CreateKeyRequest struct { + // Name is the name of the key that gets created. + Name string } -// Status describes the current state of a KMS. -type Status struct { - Name string // The name of the KMS - Endpoints []string // A set of the KMS endpoints +// DeleteKeyRequest is a structure containing fields +// and options for deleting keys. +type DeleteKeyRequest struct { + // Name is the name of the key that gets deleted. + Name string +} - // DefaultKey is the key used when no explicit key ID - // is specified. It is empty if the KMS does not support - // a default key. +// GenerateKeyRequest is a structure containing fields +// and options for generating data keys. +type GenerateKeyRequest struct { + // Name is the name of the master key used to generate + // the data key. + Name string + + // AssociatedData is optional data that is cryptographically + // associated with the generated data key. The same data + // must be provided when decrypting an encrypted data key. + // + // Typically, associated data is some metadata about the + // data key. For example, the name of the object for which + // the data key is used. + AssociatedData Context +} + +// DecryptRequest is a structure containing fields +// and options for decrypting data. +type DecryptRequest struct { + // Name is the name of the master key used decrypt + // the ciphertext. + Name string + + // Version is the version of the master used for + // decryption. If empty, the latest key version + // is used. + Version int + + // Ciphertext is the encrypted data that gets + // decrypted. + Ciphertext []byte + + // AssociatedData is the crypto. associated data. + // It must match the data used during encryption + // or data key generation. + AssociatedData Context +} + +// MACRequest is a structure containing fields +// and options for generating message authentication +// codes (MAC). +type MACRequest struct { + // Name is the name of the master key used decrypt + // the ciphertext. + Name string + + Version int + + Message []byte +} + +// Metrics is a structure containing KMS metrics. +type Metrics struct { + ReqOK uint64 `json:"kms_req_success"` // Number of requests that succeeded + ReqErr uint64 `json:"kms_req_error"` // Number of requests that failed with a defined error + ReqFail uint64 `json:"kms_req_failure"` // Number of requests that failed with an undefined error + Latency map[time.Duration]uint64 `json:"kms_resp_time"` // Latency histogram of all requests +} + +var defaultLatencyBuckets = []time.Duration{ + 10 * time.Millisecond, + 50 * time.Millisecond, + 100 * time.Millisecond, + 250 * time.Millisecond, + 500 * time.Millisecond, + 1000 * time.Millisecond, // 1s + 1500 * time.Millisecond, + 3000 * time.Millisecond, + 5000 * time.Millisecond, + 10000 * time.Millisecond, // 10s +} + +// KMS is a connection to a key management system. +// It implements various cryptographic operations, +// like data key generation and decryption. +type KMS struct { + // Type identifies the KMS implementation. Either, + // MinKMS, MinKES or Builtin. + Type Type + + // The default key, used for generating new data keys + // if no explicit GenerateKeyRequest.Name is provided. DefaultKey string - // Details provides more details about the KMS endpoint status. - // including uptime, version and available CPUs. - // Could be more in future. - Details kes.State + conn conn // Connection to the KMS + + // Metrics + reqOK, reqErr, reqFail atomic.Uint64 + latencyBuckets []time.Duration // expected to be sorted + latency []atomic.Uint64 } -// DEK is a data encryption key. It consists of a -// plaintext-ciphertext pair and the ID of the key -// used to generate the ciphertext. +// Version returns version information about the KMS. // -// The plaintext can be used for cryptographic -// operations - like encrypting some data. The -// ciphertext is the encrypted version of the -// plaintext data and can be stored on untrusted -// storage. -type DEK struct { - KeyID string - Plaintext []byte - Ciphertext []byte +// TODO(aead): refactor this API call since it does not account +// for multiple KMS/KES servers. +func (k *KMS) Version(ctx context.Context) (string, error) { + return k.conn.Version(ctx) } -var ( - _ encoding.TextMarshaler = (*DEK)(nil) - _ encoding.TextUnmarshaler = (*DEK)(nil) -) +// APIs returns a list of KMS server APIs. +// +// TODO(aead): remove this API since it's hardly useful. +func (k *KMS) APIs(ctx context.Context) ([]madmin.KMSAPI, error) { + return k.conn.APIs(ctx) +} -// MarshalText encodes the DEK's key ID and ciphertext -// as JSON. -func (d DEK) MarshalText() ([]byte, error) { - type JSON struct { - KeyID string `json:"keyid"` - Ciphertext []byte `json:"ciphertext"` +// Metrics returns a current snapshot of the KMS metrics. +func (k *KMS) Metrics(ctx context.Context) (*Metrics, error) { + latency := make(map[time.Duration]uint64, len(k.latencyBuckets)) + for i, b := range k.latencyBuckets { + latency[b] = k.latency[i].Load() } - return json.Marshal(JSON{ - KeyID: d.KeyID, - Ciphertext: d.Ciphertext, + + return &Metrics{ + ReqOK: k.reqOK.Load(), + ReqErr: k.reqErr.Load(), + ReqFail: k.reqFail.Load(), + Latency: latency, + }, nil +} + +// Status returns status information about the KMS. +// +// TODO(aead): refactor this API call since it does not account +// for multiple KMS/KES servers. +func (k *KMS) Status(ctx context.Context) (*madmin.KMSStatus, error) { + endpoints, err := k.conn.Status(ctx) + if err != nil { + return nil, err + } + + return &madmin.KMSStatus{ + Name: k.Type.String(), + DefaultKeyID: k.DefaultKey, + Endpoints: endpoints, + }, nil +} + +// CreateKey creates the master key req.Name. It returns +// ErrKeyExists if the key already exists. +func (k *KMS) CreateKey(ctx context.Context, req *CreateKeyRequest) error { + start := time.Now() + err := k.conn.CreateKey(ctx, req) + k.updateMetrics(err, time.Since(start)) + + return err +} + +// ListKeyNames returns a list of key names and a potential +// next name from where to continue a subsequent listing. +func (k *KMS) ListKeyNames(ctx context.Context, req *ListRequest) ([]string, string, error) { + if req.Prefix == "*" { + req.Prefix = "" + } + return k.conn.ListKeyNames(ctx, req) +} + +// GenerateKey generates a new data key using the master key req.Name. +// It returns ErrKeyNotFound if the key does not exist. If req.Name is +// empty, the KMS default key is used. +func (k *KMS) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) { + if req.Name == "" { + req.Name = k.DefaultKey + } + + start := time.Now() + dek, err := k.conn.GenerateKey(ctx, req) + k.updateMetrics(err, time.Since(start)) + + return dek, err +} + +// Decrypt decrypts a ciphertext using the master key req.Name. +// It returns ErrKeyNotFound if the key does not exist. +func (k *KMS) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) { + start := time.Now() + plaintext, err := k.conn.Decrypt(ctx, req) + k.updateMetrics(err, time.Since(start)) + + return plaintext, err +} + +// MAC generates the checksum of the given req.Message using the key +// with the req.Name at the KMS. +func (k *KMS) MAC(ctx context.Context, req *MACRequest) ([]byte, error) { + if req.Name == "" { + req.Name = k.DefaultKey + } + + start := time.Now() + mac, err := k.conn.MAC(ctx, req) + k.updateMetrics(err, time.Since(start)) + + return mac, err +} + +func (k *KMS) updateMetrics(err error, latency time.Duration) { + // First, update the latency histogram + // Therefore, find the first bucket that holds the counter for + // requests with a latency at least as large as the given request + // latency and update its and all subsequent counters. + bucket := slices.IndexFunc(k.latencyBuckets, func(b time.Duration) bool { return latency < b }) + if bucket < 0 { + bucket = len(k.latencyBuckets) - 1 + } + for i := bucket; i < len(k.latency); i++ { + k.latency[i].Add(1) + } + + // Next, update the request counters + if err == nil { + k.reqOK.Add(1) + return + } + + var s3Err Error + if errors.As(err, &s3Err) && s3Err.Code >= http.StatusInternalServerError { + k.reqFail.Add(1) + } else { + k.reqErr.Add(1) + } +} + +type kmsConn struct { + endpoints []string + enclave string + defaultKey string + client *kms.Client +} + +func (c *kmsConn) Version(ctx context.Context) (string, error) { + resp, err := c.client.Version(ctx, &kms.VersionRequest{}) + if len(resp) == 0 && err != nil { + return "", err + } + return resp[0].Version, nil +} + +func (c *kmsConn) APIs(ctx context.Context) ([]madmin.KMSAPI, error) { + return nil, ErrNotSupported +} + +func (c *kmsConn) Status(ctx context.Context) (map[string]madmin.ItemState, error) { + stat := make(map[string]madmin.ItemState, len(c.endpoints)) + resp, err := c.client.Version(ctx, &kms.VersionRequest{}) + + for _, r := range resp { + stat[r.Host] = madmin.ItemOnline + } + for _, e := range kms.UnwrapHostErrors(err) { + stat[e.Host] = madmin.ItemOffline + } + return stat, nil +} + +func (c *kmsConn) ListKeyNames(ctx context.Context, req *ListRequest) ([]string, string, error) { + resp, err := c.client.ListKeys(ctx, &kms.ListRequest{ + Enclave: c.enclave, + Prefix: req.Prefix, + ContinueAt: req.ContinueAt, + Limit: req.Limit, }) + if err != nil { + return nil, "", errListingKeysFailed(err) + } + + names := make([]string, 0, len(resp.Items)) + for _, item := range resp.Items { + names = append(names, item.Name) + } + return names, resp.ContinueAt, nil } -// UnmarshalText tries to decode text as JSON representation -// of a DEK and sets DEK's key ID and ciphertext to the -// decoded values. -// -// It sets DEK's plaintext to nil. -func (d *DEK) UnmarshalText(text []byte) error { - type JSON struct { - KeyID string `json:"keyid"` - Ciphertext []byte `json:"ciphertext"` +func (c *kmsConn) CreateKey(ctx context.Context, req *CreateKeyRequest) error { + if err := c.client.CreateKey(ctx, &kms.CreateKeyRequest{ + Enclave: c.enclave, + Name: req.Name, + }); err != nil { + if errors.Is(err, kms.ErrKeyExists) { + return ErrKeyExists + } + if errors.Is(err, kms.ErrPermission) { + return ErrPermission + } + return errKeyCreationFailed(err) } - var v JSON - json := jsoniter.ConfigCompatibleWithStandardLibrary - if err := json.Unmarshal(text, &v); err != nil { - return err - } - d.KeyID, d.Plaintext, d.Ciphertext = v.KeyID, nil, v.Ciphertext return nil } + +func (c *kmsConn) GenerateKey(ctx context.Context, req *GenerateKeyRequest) (DEK, error) { + aad, err := req.AssociatedData.MarshalText() + if err != nil { + return DEK{}, err + } + + name := req.Name + if name == "" { + name = c.defaultKey + } + + resp, err := c.client.GenerateKey(ctx, &kms.GenerateKeyRequest{ + Enclave: c.enclave, + Name: name, + AssociatedData: aad, + Length: 32, + }) + if err != nil { + if errors.Is(err, kms.ErrKeyNotFound) { + return DEK{}, ErrKeyNotFound + } + if errors.Is(err, kms.ErrPermission) { + return DEK{}, ErrPermission + } + return DEK{}, errKeyGenerationFailed(err) + } + + return DEK{ + KeyID: name, + Version: resp.Version, + Plaintext: resp.Plaintext, + Ciphertext: resp.Ciphertext, + }, nil +} + +func (c *kmsConn) Decrypt(ctx context.Context, req *DecryptRequest) ([]byte, error) { + aad, err := req.AssociatedData.MarshalText() + if err != nil { + return nil, err + } + + ciphertext, _ := parseCiphertext(req.Ciphertext) + resp, err := c.client.Decrypt(ctx, &kms.DecryptRequest{ + Enclave: c.enclave, + Name: req.Name, + Ciphertext: ciphertext, + AssociatedData: aad, + }) + if err != nil { + if errors.Is(err, kms.ErrKeyNotFound) { + return nil, ErrKeyNotFound + } + if errors.Is(err, kms.ErrPermission) { + return nil, ErrPermission + } + return nil, errDecryptionFailed(err) + } + return resp.Plaintext, nil +} + +// MAC generates the checksum of the given req.Message using the key +// with the req.Name at the KMS. +func (*kmsConn) MAC(context.Context, *MACRequest) ([]byte, error) { + return nil, ErrNotSupported +} diff --git a/internal/kms/policy-manager.go b/internal/kms/policy-manager.go deleted file mode 100644 index 0428065d6..000000000 --- a/internal/kms/policy-manager.go +++ /dev/null @@ -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 . - -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) -} diff --git a/internal/kms/secret-key.go b/internal/kms/secret-key.go new file mode 100644 index 000000000..f0bb0a930 --- /dev/null +++ b/internal/kms/secret-key.go @@ -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 . + +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 : 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 +} diff --git a/internal/kms/single-key_test.go b/internal/kms/secret-key_test.go similarity index 67% rename from internal/kms/single-key_test.go rename to internal/kms/secret-key_test.go index 166f56cd8..44604c93f 100644 --- a/internal/kms/single-key_test.go +++ b/internal/kms/secret-key_test.go @@ -25,16 +25,19 @@ import ( ) func TestSingleKeyRoundtrip(t *testing.T) { - KMS, err := Parse("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=") + KMS, err := ParseSecretKey("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=") if err != nil { t.Fatalf("Failed to initialize KMS: %v", err) } - key, err := KMS.GenerateKey(context.Background(), "my-key", Context{}) + key, err := KMS.GenerateKey(context.Background(), &GenerateKeyRequest{Name: "my-key"}) if err != nil { t.Fatalf("Failed to generate key: %v", err) } - plaintext, err := KMS.DecryptKey(key.KeyID, key.Ciphertext, Context{}) + plaintext, err := KMS.Decrypt(context.TODO(), &DecryptRequest{ + Name: key.KeyID, + Ciphertext: key.Ciphertext, + }) if err != nil { t.Fatalf("Failed to decrypt key: %v", err) } @@ -44,7 +47,7 @@ func TestSingleKeyRoundtrip(t *testing.T) { } func TestDecryptKey(t *testing.T) { - KMS, err := Parse("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=") + KMS, err := ParseSecretKey("my-key:eEm+JI9/q4JhH8QwKvf3LKo4DEBl6QbfvAl1CAbMIv8=") if err != nil { t.Fatalf("Failed to initialize KMS: %v", err) } @@ -54,11 +57,11 @@ func TestDecryptKey(t *testing.T) { if err != nil { t.Fatalf("Test %d: failed to decode plaintext key: %v", i, err) } - ciphertext, err := base64.StdEncoding.DecodeString(test.Ciphertext) - if err != nil { - t.Fatalf("Test %d: failed to decode ciphertext key: %v", i, err) - } - plaintext, err := KMS.DecryptKey(test.KeyID, ciphertext, test.Context) + plaintext, err := KMS.Decrypt(context.TODO(), &DecryptRequest{ + Name: test.KeyID, + Ciphertext: []byte(test.Ciphertext), + AssociatedData: test.Context, + }) if err != nil { t.Fatalf("Test %d: failed to decrypt key: %v", i, err) } @@ -77,12 +80,12 @@ var decryptKeyTests = []struct { { KeyID: "my-key", Plaintext: "zmS7NrG765UZ0ZN85oPjybelxqVvpz01vxsSpOISy2M=", - Ciphertext: "eyJhZWFkIjoiQ2hhQ2hhMjBQb2x5MTMwNSIsIml2IjoiSmJJK3Z3dll3dzFsQ2I1VnBrQUZ1UT09Iiwibm9uY2UiOiJBUmpJakp4QlNENTQxR3o4IiwiYnl0ZXMiOiJLQ2JFYzJzQTBUTHZBN2FXVFdhMjNBZGNjVmZKTXBPeHdnRzhobSs0UGFOcnhZZnkxeEZXWmcyZ0VlblZyT2d2In0=", + Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"JbI+vwvYww1lCb5VpkAFuQ==","nonce":"ARjIjJxBSD541Gz8","bytes":"KCbEc2sA0TLvA7aWTWa23AdccVfJMpOxwgG8hm+4PaNrxYfy1xFWZg2gEenVrOgv"}`, }, { KeyID: "my-key", Plaintext: "UnPWsZgVI+T4L9WGNzFlP1PsP1Z6hn2Fx8ISeZfDGnA=", - Ciphertext: "eyJhZWFkIjoiQ2hhQ2hhMjBQb2x5MTMwNSIsIml2IjoicjQreWZpVmJWSVlSMFoySTlGcSs2Zz09Iiwibm9uY2UiOiIyWXB3R3dFNTlHY1ZyYUkzIiwiYnl0ZXMiOiJrL3N2TWdsT1U3L0tnd3Y3M2hlRzM4TldXNTc1WExjRnAzU2F4UUhETWpKR1l5UkkzRml5Z3UyT2V1dEdQWE5MIn0=", + Ciphertext: `{"aead":"ChaCha20Poly1305","iv":"r4+yfiVbVIYR0Z2I9Fq+6g==","nonce":"2YpwGwE59GcVraI3","bytes":"k/svMglOU7/Kgwv73heG38NWW575XLcFp3SaxQHDMjJGYyRI3Fiygu2OeutGPXNL"}`, Context: Context{"key": "value"}, }, } diff --git a/internal/kms/single-key.go b/internal/kms/single-key.go deleted file mode 100644 index ab77775fc..000000000 --- a/internal/kms/single-key.go +++ /dev/null @@ -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 . - -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: -// -// : -// -// 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"` -} diff --git a/internal/kms/status-manager.go b/internal/kms/status-manager.go deleted file mode 100644 index d005b163f..000000000 --- a/internal/kms/status-manager.go +++ /dev/null @@ -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 . - -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) -}