storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/crypto/kes.go (about)

     1  // MinIO Cloud Storage, (C) 2019-2020 MinIO, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package crypto
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"crypto/tls"
    21  	"crypto/x509"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"net/http"
    27  	"net/url"
    28  	"os"
    29  	"path/filepath"
    30  	"strings"
    31  	"time"
    32  
    33  	jsoniter "github.com/json-iterator/go"
    34  
    35  	xhttp "storj.io/minio/cmd/http"
    36  	"storj.io/minio/pkg/kms"
    37  	xnet "storj.io/minio/pkg/net"
    38  )
    39  
    40  var json = jsoniter.ConfigCompatibleWithStandardLibrary
    41  
    42  // ErrKESKeyExists is the error returned a KES server
    43  // when a master key does exist.
    44  var ErrKESKeyExists = NewKESError(http.StatusBadRequest, "key does already exist")
    45  
    46  // KesConfig contains the configuration required
    47  // to initialize and connect to a kes server.
    48  type KesConfig struct {
    49  	Enabled bool
    50  
    51  	// The KES server endpoints.
    52  	Endpoint []string
    53  
    54  	// The path to the TLS private key used
    55  	// by MinIO to authenticate to the kes
    56  	// server during the TLS handshake (mTLS).
    57  	KeyFile string
    58  
    59  	// The path to the TLS certificate used
    60  	// by MinIO to authenticate to the kes
    61  	// server during the TLS handshake (mTLS).
    62  	//
    63  	// The kes server will also allow or deny
    64  	// access based on this certificate.
    65  	// In particular, the kes server will
    66  	// lookup the policy that corresponds to
    67  	// the identity in this certificate.
    68  	CertFile string
    69  
    70  	// Path to a file or directory containing
    71  	// the CA certificate(s) that issued / will
    72  	// issue certificates for the kes server.
    73  	//
    74  	// This is required if the TLS certificate
    75  	// of the kes server has not been issued
    76  	// (e.g. b/c it's self-signed) by a CA that
    77  	// MinIO trusts.
    78  	CAPath string
    79  
    80  	// The default key ID returned by KMS.KeyID().
    81  	DefaultKeyID string
    82  
    83  	// The HTTP transport configuration for
    84  	// the KES client.
    85  	Transport *http.Transport
    86  }
    87  
    88  // Verify verifies if the kes configuration is correct
    89  func (k KesConfig) Verify() (err error) {
    90  	switch {
    91  	case len(k.Endpoint) == 0:
    92  		err = Errorf("crypto: missing kes endpoint")
    93  	case k.CertFile == "":
    94  		err = Errorf("crypto: missing cert file")
    95  	case k.KeyFile == "":
    96  		err = Errorf("crypto: missing key file")
    97  	case k.DefaultKeyID == "":
    98  		err = Errorf("crypto: missing default key id")
    99  	}
   100  	return err
   101  }
   102  
   103  type kesService struct {
   104  	client *kesClient
   105  
   106  	endpoints    []string
   107  	defaultKeyID string
   108  }
   109  
   110  // NewKes returns a new kes KMS client. The returned KMS
   111  // uses the X.509 certificate to authenticate itself to
   112  // the kes server available at address.
   113  //
   114  // The defaultKeyID is the key ID returned when calling
   115  // KMS.KeyID().
   116  func NewKes(cfg KesConfig) (KMS, error) {
   117  	cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	if cfg.Transport.TLSClientConfig != nil && cfg.Transport.TLSClientConfig.RootCAs != nil {
   123  		if err = loadCACertificates(cfg.CAPath, cfg.Transport.TLSClientConfig.RootCAs); err != nil {
   124  			return nil, err
   125  		}
   126  	} else {
   127  		rootCAs, _ := x509.SystemCertPool()
   128  		if rootCAs == nil {
   129  			// In some systems (like Windows) system cert pool is
   130  			// not supported or no certificates are present on the
   131  			// system - so we create a new cert pool.
   132  			rootCAs = x509.NewCertPool()
   133  		}
   134  		if err = loadCACertificates(cfg.CAPath, rootCAs); err != nil {
   135  			return nil, err
   136  		}
   137  		if cfg.Transport.TLSClientConfig == nil {
   138  			cfg.Transport.TLSClientConfig = &tls.Config{
   139  				RootCAs: rootCAs,
   140  			}
   141  		} else {
   142  			cfg.Transport.TLSClientConfig.RootCAs = rootCAs
   143  		}
   144  	}
   145  	cfg.Transport.TLSClientConfig.Certificates = []tls.Certificate{cert}
   146  	cfg.Transport.TLSClientConfig.NextProtos = []string{"h2"}
   147  
   148  	return &kesService{
   149  		client: &kesClient{
   150  			endpoints: cfg.Endpoint,
   151  			httpClient: http.Client{
   152  				Transport: cfg.Transport,
   153  			},
   154  		},
   155  		endpoints:    cfg.Endpoint,
   156  		defaultKeyID: cfg.DefaultKeyID,
   157  	}, nil
   158  }
   159  
   160  func (kes *kesService) Stat() (kms.Status, error) {
   161  	return kms.Status{
   162  		Name:       "KES",
   163  		Endpoints:  kes.endpoints,
   164  		DefaultKey: kes.defaultKeyID,
   165  	}, nil
   166  }
   167  
   168  // CreateKey tries to create a new master key with the given keyID.
   169  func (kes *kesService) CreateKey(keyID string) error { return kes.client.CreateKey(keyID) }
   170  
   171  // GenerateKey returns a new plaintext key, generated by the KMS,
   172  // and a sealed version of this plaintext key encrypted using the
   173  // named key referenced by keyID. It also binds the generated key
   174  // cryptographically to the provided context.
   175  func (kes *kesService) GenerateKey(keyID string, ctx Context) (kms.DEK, error) {
   176  	if keyID == "" {
   177  		keyID = kes.defaultKeyID
   178  	}
   179  	context, err := ctx.MarshalText()
   180  	if err != nil {
   181  		return kms.DEK{}, err
   182  	}
   183  
   184  	plaintext, ciphertext, err := kes.client.GenerateDataKey(keyID, context)
   185  	if err != nil {
   186  		return kms.DEK{}, err
   187  	}
   188  	return kms.DEK{
   189  		KeyID:      keyID,
   190  		Plaintext:  plaintext,
   191  		Ciphertext: ciphertext,
   192  	}, nil
   193  }
   194  
   195  // UnsealKey returns the decrypted sealedKey as plaintext key.
   196  // Therefore it sends the sealedKey to the KMS which decrypts
   197  // it using the named key referenced by keyID and responses with
   198  // the plaintext key.
   199  //
   200  // The context must be same context as the one provided while
   201  // generating the plaintext key / sealedKey.
   202  func (kes *kesService) DecryptKey(keyID string, ciphertext []byte, ctx Context) ([]byte, error) {
   203  	context, err := ctx.MarshalText()
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  	return kes.client.DecryptDataKey(keyID, ciphertext, context)
   208  }
   209  
   210  // kesClient implements the bare minimum functionality needed for
   211  // MinIO to talk to a KES server. In particular, it implements
   212  //   • CreateKey       (API: /v1/key/create/)
   213  //   • GenerateDataKey (API: /v1/key/generate/)
   214  //   • DecryptDataKey  (API: /v1/key/decrypt/)
   215  type kesClient struct {
   216  	endpoints  []string
   217  	httpClient http.Client
   218  }
   219  
   220  // CreateKey tries to create a new cryptographic key with
   221  // the specified name.
   222  //
   223  // The key will be generated by the server. The client
   224  // application does not have the cryptographic key at
   225  // any point in time.
   226  func (c *kesClient) CreateKey(name string) error {
   227  	path := fmt.Sprintf("/v1/key/create/%s", url.PathEscape(name))
   228  	_, err := c.postRetry(path, nil, 0) // No request body and no response expected
   229  	if err != nil {
   230  		return err
   231  	}
   232  	return nil
   233  }
   234  
   235  // GenerateDataKey requests a new data key from the KES server.
   236  // On success, the KES server will respond with the plaintext key
   237  // and the ciphertext key as the plaintext key encrypted with
   238  // the key specified by name.
   239  //
   240  // The optional context is crytpo. bound to the generated data key
   241  // such that you have to provide the same context when decrypting
   242  // the data key.
   243  func (c *kesClient) GenerateDataKey(name string, context []byte) ([]byte, []byte, error) {
   244  	type Request struct {
   245  		Context []byte `json:"context"`
   246  	}
   247  	type Response struct {
   248  		Plaintext  []byte `json:"plaintext"`
   249  		Ciphertext []byte `json:"ciphertext"`
   250  	}
   251  
   252  	body, err := json.Marshal(Request{
   253  		Context: context,
   254  	})
   255  	if err != nil {
   256  		return nil, nil, err
   257  	}
   258  
   259  	const limit = 1 << 20 // A plaintext/ciphertext key pair will never be larger than 1 MB
   260  	path := fmt.Sprintf("/v1/key/generate/%s", url.PathEscape(name))
   261  	resp, err := c.postRetry(path, bytes.NewReader(body), limit)
   262  	if err != nil {
   263  		return nil, nil, err
   264  	}
   265  
   266  	var response Response
   267  	if err = json.NewDecoder(resp).Decode(&response); err != nil {
   268  		return nil, nil, err
   269  	}
   270  	return response.Plaintext, response.Ciphertext, nil
   271  }
   272  
   273  // GenerateDataKey decrypts an encrypted data key with the key
   274  // specified by name by talking to the KES server.
   275  // On success, the KES server will respond with the plaintext key.
   276  //
   277  // The optional context must match the value you provided when
   278  // generating the data key.
   279  func (c *kesClient) DecryptDataKey(name string, ciphertext, context []byte) ([]byte, error) {
   280  	type Request struct {
   281  		Ciphertext []byte `json:"ciphertext"`
   282  		Context    []byte `json:"context,omitempty"`
   283  	}
   284  	type Response struct {
   285  		Plaintext []byte `json:"plaintext"`
   286  	}
   287  
   288  	body, err := json.Marshal(Request{
   289  		Ciphertext: ciphertext,
   290  		Context:    context,
   291  	})
   292  	if err != nil {
   293  		return nil, err
   294  	}
   295  
   296  	const limit = 1 << 20 // A data key will never be larger than 1 MiB
   297  	path := fmt.Sprintf("/v1/key/decrypt/%s", url.PathEscape(name))
   298  	resp, err := c.postRetry(path, bytes.NewReader(body), limit)
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  
   303  	var response Response
   304  	if err = json.NewDecoder(resp).Decode(&response); err != nil {
   305  		return nil, err
   306  	}
   307  	return response.Plaintext, nil
   308  }
   309  
   310  // NewKESError returns a new KES API error with the given
   311  // HTTP status code and error message.
   312  //
   313  // Two errors with the same status code and
   314  // error message are equal:
   315  //   e1 == e2 // true.
   316  func NewKESError(code int, text string) error {
   317  	return kesError{
   318  		code:    code,
   319  		message: text,
   320  	}
   321  }
   322  
   323  type kesError struct {
   324  	code    int
   325  	message string
   326  }
   327  
   328  // Status returns the HTTP status code of the error.
   329  func (e kesError) Status() int { return e.code }
   330  
   331  // Status returns the error message of the error.
   332  func (e kesError) Error() string { return e.message }
   333  
   334  func parseErrorResponse(resp *http.Response) error {
   335  	if resp == nil || resp.StatusCode < 400 {
   336  		return nil
   337  	}
   338  	if resp.Body == nil {
   339  		return NewKESError(resp.StatusCode, "")
   340  	}
   341  	defer resp.Body.Close()
   342  
   343  	const MaxBodySize = 1 << 20
   344  	var size = resp.ContentLength
   345  	if size < 0 || size > MaxBodySize {
   346  		size = MaxBodySize
   347  	}
   348  
   349  	contentType := strings.TrimSpace(resp.Header.Get("Content-Type"))
   350  	if strings.HasPrefix(contentType, "application/json") {
   351  		type Response struct {
   352  			Message string `json:"message"`
   353  		}
   354  		var response Response
   355  		if err := json.NewDecoder(io.LimitReader(resp.Body, size)).Decode(&response); err != nil {
   356  			return err
   357  		}
   358  		return NewKESError(resp.StatusCode, response.Message)
   359  	}
   360  
   361  	var sb strings.Builder
   362  	if _, err := io.Copy(&sb, io.LimitReader(resp.Body, size)); err != nil {
   363  		return err
   364  	}
   365  	return NewKESError(resp.StatusCode, sb.String())
   366  }
   367  
   368  func (c *kesClient) post(url string, body io.Reader, limit int64) (io.Reader, error) {
   369  	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
   370  	defer cancel()
   371  
   372  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	req.Header.Set("Content-Type", "application/json")
   377  
   378  	resp, err := c.httpClient.Do(req)
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  	// Drain the entire body to make sure we have re-use connections
   383  	defer xhttp.DrainBody(resp.Body)
   384  
   385  	if resp.StatusCode != http.StatusOK {
   386  		return nil, parseErrorResponse(resp)
   387  	}
   388  
   389  	// We have to copy the response body due to draining.
   390  	var respBody bytes.Buffer
   391  	if _, err = io.Copy(&respBody, io.LimitReader(resp.Body, limit)); err != nil {
   392  		return nil, err
   393  	}
   394  	return &respBody, nil
   395  }
   396  
   397  func (c *kesClient) postRetry(path string, body io.ReadSeeker, limit int64) (io.Reader, error) {
   398  	retryMax := 1 + len(c.endpoints)
   399  	for i := 0; ; i++ {
   400  		if body != nil {
   401  			body.Seek(0, io.SeekStart) // seek to the beginning of the body.
   402  		}
   403  
   404  		response, err := c.post(c.endpoints[i%len(c.endpoints)]+path, body, limit)
   405  		if err == nil {
   406  			return response, nil
   407  		}
   408  
   409  		// If the error is not temp. / retryable => fail the request immediately.
   410  		if !xnet.IsNetworkOrHostDown(err, false) &&
   411  			!errors.Is(err, io.EOF) &&
   412  			!errors.Is(err, io.ErrUnexpectedEOF) &&
   413  			!errors.Is(err, context.DeadlineExceeded) {
   414  			return nil, err
   415  		}
   416  		if remain := retryMax - i; remain <= 0 { // Fail if we exceeded our retry limit.
   417  			return response, err
   418  		}
   419  
   420  		// If there are more KES instances then skip waiting and
   421  		// try the next endpoint directly.
   422  		if i < len(c.endpoints) {
   423  			continue
   424  		}
   425  		<-time.After(LinearJitterBackoff(retryWaitMin, retryWaitMax, i))
   426  	}
   427  }
   428  
   429  // loadCACertificates returns a new CertPool
   430  // that contains all system root CA certificates
   431  // and any PEM-encoded certificate(s) found at
   432  // path.
   433  //
   434  // If path is a file, loadCACertificates will
   435  // try to parse it as PEM-encoded certificate.
   436  // If this fails, it returns an error.
   437  //
   438  // If path is a directory it tries to parse each
   439  // file as PEM-encoded certificate and add it to
   440  // the CertPool. If a file is not a PEM certificate
   441  // it will be ignored.
   442  func loadCACertificates(path string, rootCAs *x509.CertPool) error {
   443  	if path == "" {
   444  		return nil
   445  	}
   446  
   447  	stat, err := os.Stat(path)
   448  	if err != nil {
   449  		if os.IsNotExist(err) || os.IsPermission(err) {
   450  			return nil
   451  		}
   452  		return Errorf("crypto: cannot open '%s': %v", path, err)
   453  	}
   454  
   455  	// If path is a file, parse as PEM-encoded certifcate
   456  	// and try to add it to the CertPool. If this fails
   457  	// return an error.
   458  	if !stat.IsDir() {
   459  		cert, err := ioutil.ReadFile(path)
   460  		if err != nil {
   461  			return err
   462  		}
   463  		if !rootCAs.AppendCertsFromPEM(cert) {
   464  			return Errorf("crypto: '%s' is not a valid PEM-encoded certificate", path)
   465  		}
   466  		return nil
   467  	}
   468  
   469  	// If path is a directory then try
   470  	// to parse each file as PEM-encoded
   471  	// certificate and add it to the CertPool.
   472  	// If a file is not a PEM-encoded certificate
   473  	// we ignore it.
   474  	files, err := ioutil.ReadDir(path)
   475  	if err != nil {
   476  		return err
   477  	}
   478  	for _, file := range files {
   479  		cert, err := ioutil.ReadFile(filepath.Join(path, file.Name()))
   480  		if err != nil {
   481  			continue // ignore files which are not readable
   482  		}
   483  		rootCAs.AppendCertsFromPEM(cert) // ignore files which are not PEM certtificates
   484  	}
   485  	return nil
   486  
   487  }