storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/signature-v4-utils.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2015, 2016, 2017 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/hmac"
    23  	"crypto/sha256"
    24  	"encoding/hex"
    25  	"io"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"strconv"
    29  	"strings"
    30  
    31  	xhttp "storj.io/minio/cmd/http"
    32  	"storj.io/minio/cmd/logger"
    33  	"storj.io/minio/pkg/auth"
    34  )
    35  
    36  // http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the
    37  // client did not calculate sha256 of the payload.
    38  const unsignedPayload = "UNSIGNED-PAYLOAD"
    39  
    40  // skipContentSha256Cksum returns true if caller needs to skip
    41  // payload checksum, false if not.
    42  func skipContentSha256Cksum(r *http.Request) bool {
    43  	var (
    44  		v  []string
    45  		ok bool
    46  	)
    47  
    48  	if isRequestPresignedSignatureV4(r) {
    49  		v, ok = r.URL.Query()[xhttp.AmzContentSha256]
    50  		if !ok {
    51  			v, ok = r.Header[xhttp.AmzContentSha256]
    52  		}
    53  	} else {
    54  		v, ok = r.Header[xhttp.AmzContentSha256]
    55  	}
    56  
    57  	// If x-amz-content-sha256 is set and the value is not
    58  	// 'UNSIGNED-PAYLOAD' we should validate the content sha256.
    59  	return !(ok && v[0] != unsignedPayload)
    60  }
    61  
    62  // Returns SHA256 for calculating canonical-request.
    63  func getContentSha256Cksum(r *http.Request, stype serviceType) string {
    64  	if stype == serviceSTS {
    65  		payload, err := ioutil.ReadAll(io.LimitReader(r.Body, stsRequestBodyLimit))
    66  		if err != nil {
    67  			logger.CriticalIf(GlobalContext, err)
    68  		}
    69  		sum256 := sha256.Sum256(payload)
    70  		r.Body = ioutil.NopCloser(bytes.NewReader(payload))
    71  		return hex.EncodeToString(sum256[:])
    72  	}
    73  
    74  	var (
    75  		defaultSha256Cksum string
    76  		v                  []string
    77  		ok                 bool
    78  	)
    79  
    80  	// For a presigned request we look at the query param for sha256.
    81  	if isRequestPresignedSignatureV4(r) {
    82  		// X-Amz-Content-Sha256, if not set in presigned requests, checksum
    83  		// will default to 'UNSIGNED-PAYLOAD'.
    84  		defaultSha256Cksum = unsignedPayload
    85  		v, ok = r.URL.Query()[xhttp.AmzContentSha256]
    86  		if !ok {
    87  			v, ok = r.Header[xhttp.AmzContentSha256]
    88  		}
    89  	} else {
    90  		// X-Amz-Content-Sha256, if not set in signed requests, checksum
    91  		// will default to sha256([]byte("")).
    92  		defaultSha256Cksum = emptySHA256
    93  		v, ok = r.Header[xhttp.AmzContentSha256]
    94  	}
    95  
    96  	// We found 'X-Amz-Content-Sha256' return the captured value.
    97  	if ok {
    98  		return v[0]
    99  	}
   100  
   101  	// We couldn't find 'X-Amz-Content-Sha256'.
   102  	return defaultSha256Cksum
   103  }
   104  
   105  // isValidRegion - verify if incoming region value is valid with configured Region.
   106  func isValidRegion(reqRegion string, confRegion string) bool {
   107  	if confRegion == "" {
   108  		return true
   109  	}
   110  	if confRegion == "US" {
   111  		confRegion = globalMinioDefaultRegion
   112  	}
   113  	// Some older s3 clients set region as "US" instead of
   114  	// globalMinioDefaultRegion, handle it.
   115  	if reqRegion == "US" {
   116  		reqRegion = globalMinioDefaultRegion
   117  	}
   118  	return reqRegion == confRegion
   119  }
   120  
   121  // check if the access key is valid and recognized, additionally
   122  // also returns if the access key is owner/admin.
   123  func checkKeyValid(ctx context.Context, accessKey string) (auth.Credentials, bool, APIErrorCode) {
   124  	var owner = true
   125  	var cred = globalActiveCred
   126  	if cred.AccessKey != accessKey {
   127  		// Check if the access key is part of users credentials.
   128  		var ok bool
   129  		if cred, ok = GlobalIAMSys.GetUser(ctx, accessKey); !ok {
   130  			return cred, false, ErrInvalidAccessKeyID
   131  		}
   132  		owner = false
   133  	}
   134  	return cred, owner, ErrNone
   135  }
   136  
   137  // sumHMAC calculate hmac between two input byte array.
   138  func sumHMAC(key []byte, data []byte) []byte {
   139  	hash := hmac.New(sha256.New, key)
   140  	hash.Write(data)
   141  	return hash.Sum(nil)
   142  }
   143  
   144  // extractSignedHeaders extract signed headers from Authorization header
   145  func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, APIErrorCode) {
   146  	reqHeaders := r.Header
   147  	reqQueries := r.URL.Query()
   148  	// find whether "host" is part of list of signed headers.
   149  	// if not return ErrUnsignedHeaders. "host" is mandatory.
   150  	if !contains(signedHeaders, "host") {
   151  		return nil, ErrUnsignedHeaders
   152  	}
   153  	extractedSignedHeaders := make(http.Header)
   154  	for _, header := range signedHeaders {
   155  		// `host` will not be found in the headers, can be found in r.Host.
   156  		// but its alway necessary that the list of signed headers containing host in it.
   157  		val, ok := reqHeaders[http.CanonicalHeaderKey(header)]
   158  		if !ok {
   159  			// try to set headers from Query String
   160  			val, ok = reqQueries[header]
   161  		}
   162  		if ok {
   163  			extractedSignedHeaders[http.CanonicalHeaderKey(header)] = val
   164  			continue
   165  		}
   166  		switch header {
   167  		case "expect":
   168  			// Golang http server strips off 'Expect' header, if the
   169  			// client sent this as part of signed headers we need to
   170  			// handle otherwise we would see a signature mismatch.
   171  			// `aws-cli` sets this as part of signed headers.
   172  			//
   173  			// According to
   174  			// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20
   175  			// Expect header is always of form:
   176  			//
   177  			//   Expect       =  "Expect" ":" 1#expectation
   178  			//   expectation  =  "100-continue" | expectation-extension
   179  			//
   180  			// So it safe to assume that '100-continue' is what would
   181  			// be sent, for the time being keep this work around.
   182  			// Adding a *TODO* to remove this later when Golang server
   183  			// doesn't filter out the 'Expect' header.
   184  			extractedSignedHeaders.Set(header, "100-continue")
   185  		case "host":
   186  			// Go http server removes "host" from Request.Header
   187  			extractedSignedHeaders.Set(header, r.Host)
   188  		case "transfer-encoding":
   189  			// Go http server removes "host" from Request.Header
   190  			extractedSignedHeaders[http.CanonicalHeaderKey(header)] = r.TransferEncoding
   191  		case "content-length":
   192  			// Signature-V4 spec excludes Content-Length from signed headers list for signature calculation.
   193  			// But some clients deviate from this rule. Hence we consider Content-Length for signature
   194  			// calculation to be compatible with such clients.
   195  			extractedSignedHeaders.Set(header, strconv.FormatInt(r.ContentLength, 10))
   196  		default:
   197  			return nil, ErrUnsignedHeaders
   198  		}
   199  	}
   200  	return extractedSignedHeaders, ErrNone
   201  }
   202  
   203  // Trim leading and trailing spaces and replace sequential spaces with one space, following Trimall()
   204  // in http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
   205  func signV4TrimAll(input string) string {
   206  	// Compress adjacent spaces (a space is determined by
   207  	// unicode.IsSpace() internally here) to one space and return
   208  	return strings.Join(strings.Fields(input), " ")
   209  }