storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/signature-v4.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 This file implements helper functions to validate AWS
    18  // Signature Version '4' authorization header.
    19  //
    20  // This package provides comprehensive helpers for following signature
    21  // types.
    22  // - Based on Authorization header.
    23  // - Based on Query parameters.
    24  // - Based on Form POST policy.
    25  package cmd
    26  
    27  import (
    28  	"bytes"
    29  	"context"
    30  	"crypto/sha256"
    31  	"crypto/subtle"
    32  	"encoding/hex"
    33  	"net/http"
    34  	"net/url"
    35  	"sort"
    36  	"strconv"
    37  	"strings"
    38  	"time"
    39  
    40  	"github.com/minio/minio-go/v7/pkg/s3utils"
    41  	"github.com/minio/minio-go/v7/pkg/set"
    42  
    43  	xhttp "storj.io/minio/cmd/http"
    44  	"storj.io/minio/cmd/logger"
    45  	"storj.io/minio/pkg/auth"
    46  )
    47  
    48  // AWS Signature Version '4' constants.
    49  const (
    50  	signV4Algorithm = "AWS4-HMAC-SHA256"
    51  	iso8601Format   = "20060102T150405Z"
    52  	yyyymmdd        = "20060102"
    53  )
    54  
    55  type serviceType string
    56  
    57  const (
    58  	serviceS3  serviceType = "s3"
    59  	serviceSTS serviceType = "sts"
    60  )
    61  
    62  // getCanonicalHeaders generate a list of request headers with their values
    63  func getCanonicalHeaders(signedHeaders http.Header) string {
    64  	var headers []string
    65  	vals := make(http.Header)
    66  	for k, vv := range signedHeaders {
    67  		headers = append(headers, strings.ToLower(k))
    68  		vals[strings.ToLower(k)] = vv
    69  	}
    70  	sort.Strings(headers)
    71  
    72  	var buf bytes.Buffer
    73  	for _, k := range headers {
    74  		buf.WriteString(k)
    75  		buf.WriteByte(':')
    76  		for idx, v := range vals[k] {
    77  			if idx > 0 {
    78  				buf.WriteByte(',')
    79  			}
    80  			buf.WriteString(signV4TrimAll(v))
    81  		}
    82  		buf.WriteByte('\n')
    83  	}
    84  	return buf.String()
    85  }
    86  
    87  // getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names
    88  func getSignedHeaders(signedHeaders http.Header) string {
    89  	var headers []string
    90  	for k := range signedHeaders {
    91  		headers = append(headers, strings.ToLower(k))
    92  	}
    93  	sort.Strings(headers)
    94  	return strings.Join(headers, ";")
    95  }
    96  
    97  // getCanonicalRequest generate a canonical request of style
    98  //
    99  // canonicalRequest =
   100  //  <HTTPMethod>\n
   101  //  <CanonicalURI>\n
   102  //  <CanonicalQueryString>\n
   103  //  <CanonicalHeaders>\n
   104  //  <SignedHeaders>\n
   105  //  <HashedPayload>
   106  //
   107  func getCanonicalRequest(extractedSignedHeaders http.Header, payload, queryStr, urlPath, method string) string {
   108  	rawQuery := strings.Replace(queryStr, "+", "%20", -1)
   109  	encodedPath := s3utils.EncodePath(urlPath)
   110  	canonicalRequest := strings.Join([]string{
   111  		method,
   112  		encodedPath,
   113  		rawQuery,
   114  		getCanonicalHeaders(extractedSignedHeaders),
   115  		getSignedHeaders(extractedSignedHeaders),
   116  		payload,
   117  	}, "\n")
   118  	return canonicalRequest
   119  }
   120  
   121  // getScope generate a string of a specific date, an AWS region, and a service.
   122  func getScope(t time.Time, region string) string {
   123  	scope := strings.Join([]string{
   124  		t.Format(yyyymmdd),
   125  		region,
   126  		string(serviceS3),
   127  		"aws4_request",
   128  	}, SlashSeparator)
   129  	return scope
   130  }
   131  
   132  // getStringToSign a string based on selected query values.
   133  func getStringToSign(canonicalRequest string, t time.Time, scope string) string {
   134  	stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n"
   135  	stringToSign = stringToSign + scope + "\n"
   136  	canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest))
   137  	stringToSign = stringToSign + hex.EncodeToString(canonicalRequestBytes[:])
   138  	return stringToSign
   139  }
   140  
   141  // getSigningKey hmac seed to calculate final signature.
   142  func getSigningKey(secretKey string, t time.Time, region string, stype serviceType) []byte {
   143  	date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format(yyyymmdd)))
   144  	regionBytes := sumHMAC(date, []byte(region))
   145  	service := sumHMAC(regionBytes, []byte(stype))
   146  	signingKey := sumHMAC(service, []byte("aws4_request"))
   147  	return signingKey
   148  }
   149  
   150  // getSignature final signature in hexadecimal form.
   151  func getSignature(signingKey []byte, stringToSign string) string {
   152  	return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign)))
   153  }
   154  
   155  // Check to see if Policy is signed correctly.
   156  func doesPolicySignatureMatch(ctx context.Context, formValues http.Header) (auth.Credentials, APIErrorCode) {
   157  	// For SignV2 - Signature field will be valid
   158  	if _, ok := formValues["Signature"]; ok {
   159  		return doesPolicySignatureV2Match(ctx, formValues)
   160  	}
   161  	return doesPolicySignatureV4Match(ctx, formValues)
   162  }
   163  
   164  // compareSignatureV4 returns true if and only if both signatures
   165  // are equal. The signatures are expected to be HEX encoded strings
   166  // according to the AWS S3 signature V4 spec.
   167  func compareSignatureV4(sig1, sig2 string) bool {
   168  	// The CTC using []byte(str) works because the hex encoding
   169  	// is unique for a sequence of bytes. See also compareSignatureV2.
   170  	return subtle.ConstantTimeCompare([]byte(sig1), []byte(sig2)) == 1
   171  }
   172  
   173  // doesPolicySignatureMatch - Verify query headers with post policy
   174  //     - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html
   175  // returns ErrNone if the signature matches.
   176  func doesPolicySignatureV4Match(ctx context.Context, formValues http.Header) (auth.Credentials, APIErrorCode) {
   177  	// Server region.
   178  	region := globalServerRegion
   179  
   180  	// Parse credential tag.
   181  	credHeader, s3Err := parseCredentialHeader("Credential="+formValues.Get(xhttp.AmzCredential), region, serviceS3)
   182  	if s3Err != ErrNone {
   183  		return auth.Credentials{}, s3Err
   184  	}
   185  
   186  	cred, _, s3Err := checkKeyValid(ctx, credHeader.accessKey)
   187  	if s3Err != ErrNone {
   188  		return cred, s3Err
   189  	}
   190  
   191  	// Get signing key.
   192  	signingKey := getSigningKey(cred.SecretKey, credHeader.scope.date, credHeader.scope.region, serviceS3)
   193  
   194  	// Get signature.
   195  	newSignature := getSignature(signingKey, formValues.Get("Policy"))
   196  
   197  	// Verify signature.
   198  	if !compareSignatureV4(newSignature, formValues.Get(xhttp.AmzSignature)) {
   199  		return cred, ErrSignatureDoesNotMatch
   200  	}
   201  	if cred.AccessKey != "" {
   202  		logger.GetReqInfo(ctx).AccessKey = cred.AccessKey
   203  	}
   204  	if cred.AccessGrant != "" {
   205  		logger.GetReqInfo(ctx).AccessGrant = cred.AccessGrant
   206  	}
   207  
   208  	// Success.
   209  	return cred, ErrNone
   210  }
   211  
   212  // doesPresignedSignatureMatch - Verify query headers with presigned signature
   213  //     - http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
   214  // returns ErrNone if the signature matches.
   215  func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region string, stype serviceType) APIErrorCode {
   216  	// Copy request
   217  	req := *r
   218  
   219  	// Parse request query string.
   220  	pSignValues, err := parsePreSignV4(req.URL.Query(), region, stype)
   221  	if err != ErrNone {
   222  		return err
   223  	}
   224  
   225  	cred, _, s3Err := checkKeyValid(req.Context(), pSignValues.Credential.accessKey)
   226  	if s3Err != ErrNone {
   227  		return s3Err
   228  	}
   229  
   230  	// Extract all the signed headers along with its values.
   231  	extractedSignedHeaders, errCode := extractSignedHeaders(pSignValues.SignedHeaders, r)
   232  	if errCode != ErrNone {
   233  		return errCode
   234  	}
   235  
   236  	// If the host which signed the request is slightly ahead in time (by less than globalMaxSkewTime) the
   237  	// request should still be allowed.
   238  	if pSignValues.Date.After(UTCNow().Add(globalMaxSkewTime)) {
   239  		return ErrRequestNotReadyYet
   240  	}
   241  
   242  	if UTCNow().Sub(pSignValues.Date) > pSignValues.Expires {
   243  		return ErrExpiredPresignRequest
   244  	}
   245  
   246  	// Save the date and expires.
   247  	t := pSignValues.Date
   248  	expireSeconds := int(pSignValues.Expires / time.Second)
   249  
   250  	// Construct new query.
   251  	query := make(url.Values)
   252  	clntHashedPayload := req.URL.Query().Get(xhttp.AmzContentSha256)
   253  	if clntHashedPayload != "" {
   254  		query.Set(xhttp.AmzContentSha256, hashedPayload)
   255  	}
   256  
   257  	token := req.URL.Query().Get(xhttp.AmzSecurityToken)
   258  	if token != "" {
   259  		query.Set(xhttp.AmzSecurityToken, cred.SessionToken)
   260  	}
   261  
   262  	query.Set(xhttp.AmzAlgorithm, signV4Algorithm)
   263  
   264  	// Construct the query.
   265  	query.Set(xhttp.AmzDate, t.Format(iso8601Format))
   266  	query.Set(xhttp.AmzExpires, strconv.Itoa(expireSeconds))
   267  	query.Set(xhttp.AmzSignedHeaders, getSignedHeaders(extractedSignedHeaders))
   268  	query.Set(xhttp.AmzCredential, cred.AccessKey+SlashSeparator+pSignValues.Credential.getScope())
   269  
   270  	defaultSigParams := set.CreateStringSet(
   271  		xhttp.AmzContentSha256,
   272  		xhttp.AmzSecurityToken,
   273  		xhttp.AmzAlgorithm,
   274  		xhttp.AmzDate,
   275  		xhttp.AmzExpires,
   276  		xhttp.AmzSignedHeaders,
   277  		xhttp.AmzCredential,
   278  		xhttp.AmzSignature,
   279  	)
   280  
   281  	// Add missing query parameters if any provided in the request URL
   282  	for k, v := range req.URL.Query() {
   283  		if !defaultSigParams.Contains(k) {
   284  			query[k] = v
   285  		}
   286  	}
   287  
   288  	// Get the encoded query.
   289  	encodedQuery := query.Encode()
   290  
   291  	// Verify if date query is same.
   292  	if req.URL.Query().Get(xhttp.AmzDate) != query.Get(xhttp.AmzDate) {
   293  		return ErrSignatureDoesNotMatch
   294  	}
   295  	// Verify if expires query is same.
   296  	if req.URL.Query().Get(xhttp.AmzExpires) != query.Get(xhttp.AmzExpires) {
   297  		return ErrSignatureDoesNotMatch
   298  	}
   299  	// Verify if signed headers query is same.
   300  	if req.URL.Query().Get(xhttp.AmzSignedHeaders) != query.Get(xhttp.AmzSignedHeaders) {
   301  		return ErrSignatureDoesNotMatch
   302  	}
   303  	// Verify if credential query is same.
   304  	if req.URL.Query().Get(xhttp.AmzCredential) != query.Get(xhttp.AmzCredential) {
   305  		return ErrSignatureDoesNotMatch
   306  	}
   307  	// Verify if sha256 payload query is same.
   308  	if clntHashedPayload != "" && clntHashedPayload != query.Get(xhttp.AmzContentSha256) {
   309  		return ErrContentSHA256Mismatch
   310  	}
   311  	// Verify if security token is correct.
   312  	if token != "" && subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
   313  		return ErrInvalidToken
   314  	}
   315  
   316  	/// Verify finally if signature is same.
   317  
   318  	// Get canonical request.
   319  	presignedCanonicalReq := getCanonicalRequest(extractedSignedHeaders, hashedPayload, encodedQuery, req.URL.Path, req.Method)
   320  
   321  	// Get string to sign from canonical request.
   322  	presignedStringToSign := getStringToSign(presignedCanonicalReq, t, pSignValues.Credential.getScope())
   323  
   324  	// Get hmac presigned signing key.
   325  	presignedSigningKey := getSigningKey(cred.SecretKey, pSignValues.Credential.scope.date,
   326  		pSignValues.Credential.scope.region, stype)
   327  
   328  	// Get new signature.
   329  	newSignature := getSignature(presignedSigningKey, presignedStringToSign)
   330  
   331  	// Verify signature.
   332  	if !compareSignatureV4(req.URL.Query().Get(xhttp.AmzSignature), newSignature) {
   333  		return ErrSignatureDoesNotMatch
   334  	}
   335  	return ErrNone
   336  }
   337  
   338  // doesSignatureMatch - Verify authorization header with calculated header in accordance with
   339  //     - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
   340  // returns ErrNone if signature matches.
   341  func doesSignatureMatch(hashedPayload string, r *http.Request, region string, stype serviceType) APIErrorCode {
   342  	// Copy request.
   343  	req := *r
   344  
   345  	// Save authorization header.
   346  	v4Auth := req.Header.Get(xhttp.Authorization)
   347  
   348  	// Parse signature version '4' header.
   349  	signV4Values, err := parseSignV4(v4Auth, region, stype)
   350  	if err != ErrNone {
   351  		return err
   352  	}
   353  
   354  	// Extract all the signed headers along with its values.
   355  	extractedSignedHeaders, errCode := extractSignedHeaders(signV4Values.SignedHeaders, r)
   356  	if errCode != ErrNone {
   357  		return errCode
   358  	}
   359  
   360  	cred, _, s3Err := checkKeyValid(req.Context(), signV4Values.Credential.accessKey)
   361  	if s3Err != ErrNone {
   362  		return s3Err
   363  	}
   364  
   365  	// Extract date, if not present throw error.
   366  	var date string
   367  	if date = req.Header.Get(xhttp.AmzDate); date == "" {
   368  		if date = r.Header.Get(xhttp.Date); date == "" {
   369  			return ErrMissingDateHeader
   370  		}
   371  	}
   372  
   373  	// Parse date header.
   374  	t, e := time.Parse(iso8601Format, date)
   375  	if e != nil {
   376  		return ErrMissingDateHeader
   377  	}
   378  
   379  	// Query string.
   380  	queryStr := req.URL.Query().Encode()
   381  
   382  	// Get canonical request.
   383  	canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method)
   384  
   385  	// Get string to sign from canonical request.
   386  	stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope())
   387  
   388  	// Get hmac signing key.
   389  	signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date,
   390  		signV4Values.Credential.scope.region, stype)
   391  
   392  	// Calculate signature.
   393  	newSignature := getSignature(signingKey, stringToSign)
   394  
   395  	// Verify if signature match.
   396  	if !compareSignatureV4(newSignature, signV4Values.Signature) {
   397  		return ErrSignatureDoesNotMatch
   398  	}
   399  
   400  	// Return error none.
   401  	return ErrNone
   402  }