Skip to content

Commit

Permalink
KMS Admin-API: add route and handler for KMS key info
Browse files Browse the repository at this point in the history
This commit adds an admin API route and handler for
requesting status information about a KMS key.

Therefore, the client specifies the KMS key ID (when
empty / not set the server takes the currently configured
default key-ID) and the server tries to perform a dummy encryption
and decryption operation. If both succeed we know that the
server can access the KMS and has permissions to generate and
decrypt data keys (policy is set correctly).
  • Loading branch information
Andreas Auernhammer committed Jul 31, 2019
1 parent 041a812 commit d0eadbd
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 0 deletions.
69 changes: 69 additions & 0 deletions cmd/admin-handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cmd
import (
"bytes"
"context"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"errors"
Expand All @@ -34,6 +35,7 @@ import (
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

"github.com/minio/minio/cmd/crypto"
xhttp "github.com/minio/minio/cmd/http"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/cpu"
Expand Down Expand Up @@ -1519,3 +1521,70 @@ func (a adminAPIHandlers) TraceHandler(w http.ResponseWriter, r *http.Request) {
}
}
}

// KMSKeyInfoHandler - GET /minio/admin/v1/kms/key/info?key-id=<master-key-id>
func (a adminAPIHandlers) KMSKeyInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := newContext(r, w, "KMSKeyInfoHandler")

objectAPI := validateAdminReq(ctx, w, r)
if objectAPI == nil {
return
}

if GlobalKMS == nil {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrKMSNotConfigured), r.URL)
return
}

keyID, ok := mux.Vars(r)["key-id"]
if !ok || keyID == "" {
keyID = globalKMSKeyID
}
var response = madmin.KMSKeyInfoResponse{
KeyID: keyID,
}

kmsContext := crypto.Context{"MinIO admin API": "KMSKeyInfoHandler"} // Context for a test key operation
// 1. Generate a new key using the KMS.
key, sealedKey, err := GlobalKMS.GenerateKey(keyID, kmsContext)
if err != nil {
response.Status.EncryptionErr = err.Error()
resp, err := json.Marshal(response)
if err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
return
}
writeSuccessResponseJSON(w, resp)
return
}
// 2. Verify that we can indeed decrypt the (encrypted) key
decryptedKey, err := GlobalKMS.UnsealKey(keyID, sealedKey, kmsContext)
if err != nil {
response.Status.DecryptionErr = err.Error()
resp, err := json.Marshal(response)
if err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
return
}
writeSuccessResponseJSON(w, resp)
return
}
// 3. Compare generated key with decrypted key
if subtle.ConstantTimeCompare(key[:], decryptedKey[:]) != 1 {
response.Status.DecryptionErr = "The generated and the decrypted data key do not match"
resp, err := json.Marshal(response)
if err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
return
}
writeSuccessResponseJSON(w, resp)
return
}

resp, err := json.Marshal(response)
if err != nil {
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), err.Error(), r.URL)
return
}
writeSuccessResponseJSON(w, resp)
}
5 changes: 5 additions & 0 deletions cmd/admin-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool)

// HTTP Trace
adminV1Router.Methods(http.MethodGet).Path("/trace").HandlerFunc(adminAPI.TraceHandler)

// -- KMS APIs --
//
adminV1Router.Methods(http.MethodGet).Path("/kms/key/info").HandlerFunc(httpTraceAll(adminAPI.KMSKeyInfoHandler))

// If none of the routes match, return error.
adminV1Router.NotFoundHandler = http.HandlerFunc(httpTraceHdrs(notFoundHandlerJSON))
}
55 changes: 55 additions & 0 deletions pkg/madmin/examples/kms-status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// +build ignore

/*
* MinIO Cloud Storage, (C) 2018 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package main

import (
"log"

"github.com/minio/minio/pkg/madmin"
)

func main() {
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY and my-bucketname are
// dummy values, please replace them with original values.

// API requests are secure (HTTPS) if secure=true and insecure (HTTPS) otherwise.
// New returns an MinIO Admin client object.
madmClnt, err := madmin.New("your-minio.example.com:9000", "YOUR-ACCESSKEYID", "YOUR-SECRETACCESSKEY", true)
if err != nil {
log.Fatalln(err)
}

info, err := madmClnt.GetKeyInfo("") // empty string refers to the default master key
if err != nil {
log.Fatalln(err)
}

log.Printf("Key: %s\n", info.KeyID)
if info.Status.EncryptionErr == "" {
log.Println("\t • Encryption ✔")
}else {
log.Printf("\t • Encryption failed: %s\n", info.Status.EncryptionErr)
}
if info.Status.DecryptionErr == "" {
log.Println("\t • Decryption ✔")
}else {
log.Printf("\t • Decryption failed: %s\n", info.Status.DecryptionErr)
}
}
66 changes: 66 additions & 0 deletions pkg/madmin/kms-commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* MinIO Cloud Storage, (C) 2019 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package madmin

import (
"encoding/json"
"net/http"
"net/url"
)

// GetKeyInfo requests information about the key referenced by keyID
// from the KMS connected to a MinIO by performing a Admin-API request.
// It basically hits the `/minio/admin/v1/kms/key/info` API endpoint.
func (adm *AdminClient) GetKeyInfo(keyID string) (*KMSKeyInfoResponse, error) {
// GET /minio/admin/v1/kms/key/info?key-id=<keyID>
qv := url.Values{}
qv.Set("key-id", keyID)
reqData := requestData{
relPath: "/v1/kms/key/info",
queryValues: qv,
}

resp, err := adm.executeMethod("GET", reqData)
defer closeResponse(resp)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, httpRespToErrorResponse(resp)
}
var keyInfo KMSKeyInfoResponse
if err = json.NewDecoder(resp.Body).Decode(&keyInfo); err != nil {
return nil, err
}
return &keyInfo, nil
}

// KMSKeyStatus is the status of a particular key at the KMS.
// It indicates whether the key can be used by the MinIO server
// for encryption as well as decryption.
type KMSKeyStatus struct {
EncryptionErr string `json:"encryption-error,omitempty"` // An empty error == success
DecryptionErr string `json:"decryption-error,omitempty"` // An empty error == success
}

// KMSKeyInfoResponse is the response returned by a MinIO server
// when the GetKeyInfo() call returns successfully. It contains
// status information about one particular key at the KMS.
type KMSKeyInfoResponse struct {
KeyID string `json:"key-id"`
Status KMSKeyStatus `json:"status"`
}

0 comments on commit d0eadbd

Please sign in to comment.