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 }