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

     1  /*
     2   * MinIO Cloud Storage, (C) 2015 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  	"net/http"
    21  	"net/url"
    22  	"strings"
    23  	"time"
    24  
    25  	xhttp "storj.io/minio/cmd/http"
    26  	"storj.io/minio/pkg/auth"
    27  )
    28  
    29  // credentialHeader data type represents structured form of Credential
    30  // string from authorization header.
    31  type credentialHeader struct {
    32  	accessKey string
    33  	scope     struct {
    34  		date    time.Time
    35  		region  string
    36  		service string
    37  		request string
    38  	}
    39  }
    40  
    41  // Return scope string.
    42  func (c credentialHeader) getScope() string {
    43  	return strings.Join([]string{
    44  		c.scope.date.Format(yyyymmdd),
    45  		c.scope.region,
    46  		c.scope.service,
    47  		c.scope.request,
    48  	}, SlashSeparator)
    49  }
    50  
    51  func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth.Credentials, bool, APIErrorCode) {
    52  	ch, s3Err := parseCredentialHeader("Credential="+r.URL.Query().Get(xhttp.AmzCredential), region, stype)
    53  	if s3Err != ErrNone {
    54  		// Strip off the Algorithm prefix.
    55  		v4Auth := strings.TrimPrefix(r.Header.Get("Authorization"), signV4Algorithm)
    56  		authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
    57  		if len(authFields) != 3 {
    58  			return auth.Credentials{}, false, ErrMissingFields
    59  		}
    60  		ch, s3Err = parseCredentialHeader(authFields[0], region, stype)
    61  		if s3Err != ErrNone {
    62  			return auth.Credentials{}, false, s3Err
    63  		}
    64  	}
    65  	return checkKeyValid(r.Context(), ch.accessKey)
    66  }
    67  
    68  // parse credentialHeader string into its structured form.
    69  func parseCredentialHeader(credElement string, region string, stype serviceType) (ch credentialHeader, aec APIErrorCode) {
    70  	creds := strings.SplitN(strings.TrimSpace(credElement), "=", 2)
    71  	if len(creds) != 2 {
    72  		return ch, ErrMissingFields
    73  	}
    74  	if creds[0] != "Credential" {
    75  		return ch, ErrMissingCredTag
    76  	}
    77  	credElements := strings.Split(strings.TrimSpace(creds[1]), SlashSeparator)
    78  	if len(credElements) < 5 {
    79  		return ch, ErrCredMalformed
    80  	}
    81  	accessKey := strings.Join(credElements[:len(credElements)-4], SlashSeparator) // The access key may contain one or more `/`
    82  	if !auth.IsAccessKeyValid(accessKey) {
    83  		return ch, ErrInvalidAccessKeyID
    84  	}
    85  	// Save access key id.
    86  	cred := credentialHeader{
    87  		accessKey: accessKey,
    88  	}
    89  	credElements = credElements[len(credElements)-4:]
    90  	var e error
    91  	cred.scope.date, e = time.Parse(yyyymmdd, credElements[0])
    92  	if e != nil {
    93  		return ch, ErrMalformedCredentialDate
    94  	}
    95  
    96  	cred.scope.region = credElements[1]
    97  	// Verify if region is valid.
    98  	sRegion := cred.scope.region
    99  	// Region is set to be empty, we use whatever was sent by the
   100  	// request and proceed further. This is a work-around to address
   101  	// an important problem for ListBuckets() getting signed with
   102  	// different regions.
   103  	if region == "" {
   104  		region = sRegion
   105  	}
   106  	// Should validate region, only if region is set.
   107  	if !isValidRegion(sRegion, region) {
   108  		return ch, ErrAuthorizationHeaderMalformed
   109  
   110  	}
   111  	if credElements[2] != string(stype) {
   112  		switch stype {
   113  		case serviceSTS:
   114  			return ch, ErrInvalidServiceSTS
   115  		}
   116  		return ch, ErrInvalidServiceS3
   117  	}
   118  	cred.scope.service = credElements[2]
   119  	if credElements[3] != "aws4_request" {
   120  		return ch, ErrInvalidRequestVersion
   121  	}
   122  	cred.scope.request = credElements[3]
   123  	return cred, ErrNone
   124  }
   125  
   126  // Parse signature from signature tag.
   127  func parseSignature(signElement string) (string, APIErrorCode) {
   128  	signFields := strings.Split(strings.TrimSpace(signElement), "=")
   129  	if len(signFields) != 2 {
   130  		return "", ErrMissingFields
   131  	}
   132  	if signFields[0] != "Signature" {
   133  		return "", ErrMissingSignTag
   134  	}
   135  	if signFields[1] == "" {
   136  		return "", ErrMissingFields
   137  	}
   138  	signature := signFields[1]
   139  	return signature, ErrNone
   140  }
   141  
   142  // Parse slice of signed headers from signed headers tag.
   143  func parseSignedHeader(signedHdrElement string) ([]string, APIErrorCode) {
   144  	signedHdrFields := strings.Split(strings.TrimSpace(signedHdrElement), "=")
   145  	if len(signedHdrFields) != 2 {
   146  		return nil, ErrMissingFields
   147  	}
   148  	if signedHdrFields[0] != "SignedHeaders" {
   149  		return nil, ErrMissingSignHeadersTag
   150  	}
   151  	if signedHdrFields[1] == "" {
   152  		return nil, ErrMissingFields
   153  	}
   154  	signedHeaders := strings.Split(signedHdrFields[1], ";")
   155  	return signedHeaders, ErrNone
   156  }
   157  
   158  // signValues data type represents structured form of AWS Signature V4 header.
   159  type signValues struct {
   160  	Credential    credentialHeader
   161  	SignedHeaders []string
   162  	Signature     string
   163  }
   164  
   165  // preSignValues data type represents structued form of AWS Signature V4 query string.
   166  type preSignValues struct {
   167  	signValues
   168  	Date    time.Time
   169  	Expires time.Duration
   170  }
   171  
   172  // Parses signature version '4' query string of the following form.
   173  //
   174  //   querystring = X-Amz-Algorithm=algorithm
   175  //   querystring += &X-Amz-Credential= urlencode(accessKey + '/' + credential_scope)
   176  //   querystring += &X-Amz-Date=date
   177  //   querystring += &X-Amz-Expires=timeout interval
   178  //   querystring += &X-Amz-SignedHeaders=signed_headers
   179  //   querystring += &X-Amz-Signature=signature
   180  //
   181  // verifies if any of the necessary query params are missing in the presigned request.
   182  func doesV4PresignParamsExist(query url.Values) APIErrorCode {
   183  	v4PresignQueryParams := []string{xhttp.AmzAlgorithm, xhttp.AmzCredential, xhttp.AmzSignature, xhttp.AmzDate, xhttp.AmzSignedHeaders, xhttp.AmzExpires}
   184  	for _, v4PresignQueryParam := range v4PresignQueryParams {
   185  		if _, ok := query[v4PresignQueryParam]; !ok {
   186  			return ErrInvalidQueryParams
   187  		}
   188  	}
   189  	return ErrNone
   190  }
   191  
   192  // Parses all the presigned signature values into separate elements.
   193  func parsePreSignV4(query url.Values, region string, stype serviceType) (psv preSignValues, aec APIErrorCode) {
   194  	// verify whether the required query params exist.
   195  	aec = doesV4PresignParamsExist(query)
   196  	if aec != ErrNone {
   197  		return psv, aec
   198  	}
   199  
   200  	// Verify if the query algorithm is supported or not.
   201  	if query.Get(xhttp.AmzAlgorithm) != signV4Algorithm {
   202  		return psv, ErrInvalidQuerySignatureAlgo
   203  	}
   204  
   205  	// Initialize signature version '4' structured header.
   206  	preSignV4Values := preSignValues{}
   207  
   208  	// Save credential.
   209  	preSignV4Values.Credential, aec = parseCredentialHeader("Credential="+query.Get(xhttp.AmzCredential), region, stype)
   210  	if aec != ErrNone {
   211  		return psv, aec
   212  	}
   213  
   214  	var e error
   215  	// Save date in native time.Time.
   216  	preSignV4Values.Date, e = time.Parse(iso8601Format, query.Get(xhttp.AmzDate))
   217  	if e != nil {
   218  		return psv, ErrMalformedPresignedDate
   219  	}
   220  
   221  	// Save expires in native time.Duration.
   222  	preSignV4Values.Expires, e = time.ParseDuration(query.Get(xhttp.AmzExpires) + "s")
   223  	if e != nil {
   224  		return psv, ErrMalformedExpires
   225  	}
   226  
   227  	if preSignV4Values.Expires < 0 {
   228  		return psv, ErrNegativeExpires
   229  	}
   230  
   231  	// Check if Expiry time is less than 7 days (value in seconds).
   232  	if preSignV4Values.Expires.Seconds() > 604800 {
   233  		return psv, ErrMaximumExpires
   234  	}
   235  
   236  	// Save signed headers.
   237  	preSignV4Values.SignedHeaders, aec = parseSignedHeader("SignedHeaders=" + query.Get(xhttp.AmzSignedHeaders))
   238  	if aec != ErrNone {
   239  		return psv, aec
   240  	}
   241  
   242  	// Save signature.
   243  	preSignV4Values.Signature, aec = parseSignature("Signature=" + query.Get(xhttp.AmzSignature))
   244  	if aec != ErrNone {
   245  		return psv, aec
   246  	}
   247  
   248  	// Return structed form of signature query string.
   249  	return preSignV4Values, ErrNone
   250  }
   251  
   252  // Parses signature version '4' header of the following form.
   253  //
   254  //    Authorization: algorithm Credential=accessKeyID/credScope, \
   255  //            SignedHeaders=signedHeaders, Signature=signature
   256  //
   257  func parseSignV4(v4Auth string, region string, stype serviceType) (sv signValues, aec APIErrorCode) {
   258  	// credElement is fetched first to skip replacing the space in access key.
   259  	credElement := strings.TrimPrefix(strings.Split(strings.TrimSpace(v4Auth), ",")[0], signV4Algorithm)
   260  	// Replace all spaced strings, some clients can send spaced
   261  	// parameters and some won't. So we pro-actively remove any spaces
   262  	// to make parsing easier.
   263  	v4Auth = strings.Replace(v4Auth, " ", "", -1)
   264  	if v4Auth == "" {
   265  		return sv, ErrAuthHeaderEmpty
   266  	}
   267  
   268  	// Verify if the header algorithm is supported or not.
   269  	if !strings.HasPrefix(v4Auth, signV4Algorithm) {
   270  		return sv, ErrSignatureVersionNotSupported
   271  	}
   272  
   273  	// Strip off the Algorithm prefix.
   274  	v4Auth = strings.TrimPrefix(v4Auth, signV4Algorithm)
   275  	authFields := strings.Split(strings.TrimSpace(v4Auth), ",")
   276  	if len(authFields) != 3 {
   277  		return sv, ErrMissingFields
   278  	}
   279  
   280  	// Initialize signature version '4' structured header.
   281  	signV4Values := signValues{}
   282  
   283  	var s3Err APIErrorCode
   284  	// Save credentail values.
   285  	signV4Values.Credential, s3Err = parseCredentialHeader(strings.TrimSpace(credElement), region, stype)
   286  	if s3Err != ErrNone {
   287  		return sv, s3Err
   288  	}
   289  
   290  	// Save signed headers.
   291  	signV4Values.SignedHeaders, s3Err = parseSignedHeader(authFields[1])
   292  	if s3Err != ErrNone {
   293  		return sv, s3Err
   294  	}
   295  
   296  	// Save signature.
   297  	signV4Values.Signature, s3Err = parseSignature(authFields[2])
   298  	if s3Err != ErrNone {
   299  		return sv, s3Err
   300  	}
   301  
   302  	// Return the structure here.
   303  	return signV4Values, ErrNone
   304  }