github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/signature-v4-parser.go (about)

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