github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/signature-v4_test.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  	"context"
    22  	"fmt"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"testing"
    27  	"time"
    28  )
    29  
    30  func niceError(code APIErrorCode) string {
    31  	// Special-handle ErrNone
    32  	if code == ErrNone {
    33  		return "ErrNone"
    34  	}
    35  
    36  	return fmt.Sprintf("%s (%s)", errorCodes[code].Code, errorCodes[code].Description)
    37  }
    38  
    39  func TestDoesPolicySignatureMatch(t *testing.T) {
    40  	credentialTemplate := "%s/%s/%s/s3/aws4_request"
    41  	now := UTCNow()
    42  	accessKey := globalActiveCred.AccessKey
    43  
    44  	testCases := []struct {
    45  		form     http.Header
    46  		expected APIErrorCode
    47  	}{
    48  		// (0) It should fail if 'X-Amz-Credential' is missing.
    49  		{
    50  			form:     http.Header{},
    51  			expected: ErrCredMalformed,
    52  		},
    53  		// (1) It should fail if the access key is incorrect.
    54  		{
    55  			form: http.Header{
    56  				"X-Amz-Credential": []string{fmt.Sprintf(credentialTemplate, "EXAMPLEINVALIDEXAMPL", now.Format(yyyymmdd), globalMinioDefaultRegion)},
    57  			},
    58  			expected: ErrInvalidAccessKeyID,
    59  		},
    60  		// (2) It should fail with a bad signature.
    61  		{
    62  			form: http.Header{
    63  				"X-Amz-Credential": []string{fmt.Sprintf(credentialTemplate, accessKey, now.Format(yyyymmdd), globalMinioDefaultRegion)},
    64  				"X-Amz-Date":       []string{now.Format(iso8601Format)},
    65  				"X-Amz-Signature":  []string{"invalidsignature"},
    66  				"Policy":           []string{"policy"},
    67  			},
    68  			expected: ErrSignatureDoesNotMatch,
    69  		},
    70  		// (3) It should succeed if everything is correct.
    71  		{
    72  			form: http.Header{
    73  				"X-Amz-Credential": []string{
    74  					fmt.Sprintf(credentialTemplate, accessKey, now.Format(yyyymmdd), globalMinioDefaultRegion),
    75  				},
    76  				"X-Amz-Date": []string{now.Format(iso8601Format)},
    77  				"X-Amz-Signature": []string{
    78  					getSignature(getSigningKey(globalActiveCred.SecretKey, now,
    79  						globalMinioDefaultRegion, serviceS3), "policy"),
    80  				},
    81  				"Policy": []string{"policy"},
    82  			},
    83  			expected: ErrNone,
    84  		},
    85  	}
    86  
    87  	// Run each test case individually.
    88  	for i, testCase := range testCases {
    89  		_, code := doesPolicySignatureMatch(testCase.form)
    90  		if code != testCase.expected {
    91  			t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(code))
    92  		}
    93  	}
    94  }
    95  
    96  func TestDoesPresignedSignatureMatch(t *testing.T) {
    97  	ctx, cancel := context.WithCancel(context.Background())
    98  	defer cancel()
    99  
   100  	obj, fsDir, err := prepareFS(ctx)
   101  	if err != nil {
   102  		t.Fatal(err)
   103  	}
   104  	defer os.RemoveAll(fsDir)
   105  	if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil {
   106  		t.Fatal(err)
   107  	}
   108  
   109  	// sha256 hash of "payload"
   110  	payloadSHA256 := "239f59ed55e737c77147cf55ad0c1b030b6d7ee748a7426952f9b852d5a935e5"
   111  	now := UTCNow()
   112  	credentialTemplate := "%s/%s/%s/s3/aws4_request"
   113  
   114  	region := globalSite.Region
   115  	accessKeyID := globalActiveCred.AccessKey
   116  	testCases := []struct {
   117  		queryParams map[string]string
   118  		headers     map[string]string
   119  		region      string
   120  		expected    APIErrorCode
   121  	}{
   122  		// (0) Should error without a set URL query.
   123  		{
   124  			region:   globalMinioDefaultRegion,
   125  			expected: ErrInvalidQueryParams,
   126  		},
   127  		// (1) Should error on an invalid access key.
   128  		{
   129  			queryParams: map[string]string{
   130  				"X-Amz-Algorithm":     signV4Algorithm,
   131  				"X-Amz-Date":          now.Format(iso8601Format),
   132  				"X-Amz-Expires":       "60",
   133  				"X-Amz-Signature":     "badsignature",
   134  				"X-Amz-SignedHeaders": "host;x-amz-content-sha256;x-amz-date",
   135  				"X-Amz-Credential":    fmt.Sprintf(credentialTemplate, "Z7IXGOO6BZ0REAN1Q26I", now.Format(yyyymmdd), "us-west-1"),
   136  			},
   137  			region:   "us-west-1",
   138  			expected: ErrInvalidAccessKeyID,
   139  		},
   140  		// (2) Should NOT fail with an invalid region if it doesn't verify it.
   141  		{
   142  			queryParams: map[string]string{
   143  				"X-Amz-Algorithm":      signV4Algorithm,
   144  				"X-Amz-Date":           now.Format(iso8601Format),
   145  				"X-Amz-Expires":        "60",
   146  				"X-Amz-Signature":      "badsignature",
   147  				"X-Amz-SignedHeaders":  "host;x-amz-content-sha256;x-amz-date",
   148  				"X-Amz-Credential":     fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), "us-west-1"),
   149  				"X-Amz-Content-Sha256": payloadSHA256,
   150  			},
   151  			region:   "us-west-1",
   152  			expected: ErrUnsignedHeaders,
   153  		},
   154  		// (3) Should fail to extract headers if the host header is not signed.
   155  		{
   156  			queryParams: map[string]string{
   157  				"X-Amz-Algorithm":      signV4Algorithm,
   158  				"X-Amz-Date":           now.Format(iso8601Format),
   159  				"X-Amz-Expires":        "60",
   160  				"X-Amz-Signature":      "badsignature",
   161  				"X-Amz-SignedHeaders":  "x-amz-content-sha256;x-amz-date",
   162  				"X-Amz-Credential":     fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
   163  				"X-Amz-Content-Sha256": payloadSHA256,
   164  			},
   165  			region:   region,
   166  			expected: ErrUnsignedHeaders,
   167  		},
   168  		// (4) Should give an expired request if it has expired.
   169  		{
   170  			queryParams: map[string]string{
   171  				"X-Amz-Algorithm":      signV4Algorithm,
   172  				"X-Amz-Date":           now.AddDate(0, 0, -2).Format(iso8601Format),
   173  				"X-Amz-Expires":        "60",
   174  				"X-Amz-Signature":      "badsignature",
   175  				"X-Amz-SignedHeaders":  "host;x-amz-content-sha256;x-amz-date",
   176  				"X-Amz-Credential":     fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
   177  				"X-Amz-Content-Sha256": payloadSHA256,
   178  			},
   179  			headers: map[string]string{
   180  				"X-Amz-Date":           now.AddDate(0, 0, -2).Format(iso8601Format),
   181  				"X-Amz-Content-Sha256": payloadSHA256,
   182  			},
   183  			region:   region,
   184  			expected: ErrExpiredPresignRequest,
   185  		},
   186  		// (5) Should error if the signature is incorrect.
   187  		{
   188  			queryParams: map[string]string{
   189  				"X-Amz-Algorithm":      signV4Algorithm,
   190  				"X-Amz-Date":           now.Format(iso8601Format),
   191  				"X-Amz-Expires":        "60",
   192  				"X-Amz-Signature":      "badsignature",
   193  				"X-Amz-SignedHeaders":  "host;x-amz-content-sha256;x-amz-date",
   194  				"X-Amz-Credential":     fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
   195  				"X-Amz-Content-Sha256": payloadSHA256,
   196  			},
   197  			headers: map[string]string{
   198  				"X-Amz-Date":           now.Format(iso8601Format),
   199  				"X-Amz-Content-Sha256": payloadSHA256,
   200  			},
   201  			region:   region,
   202  			expected: ErrSignatureDoesNotMatch,
   203  		},
   204  		// (6) Should error if the request is not ready yet, ie X-Amz-Date is in the future.
   205  		{
   206  			queryParams: map[string]string{
   207  				"X-Amz-Algorithm":      signV4Algorithm,
   208  				"X-Amz-Date":           now.Add(1 * time.Hour).Format(iso8601Format),
   209  				"X-Amz-Expires":        "60",
   210  				"X-Amz-Signature":      "badsignature",
   211  				"X-Amz-SignedHeaders":  "host;x-amz-content-sha256;x-amz-date",
   212  				"X-Amz-Credential":     fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
   213  				"X-Amz-Content-Sha256": payloadSHA256,
   214  			},
   215  			headers: map[string]string{
   216  				"X-Amz-Date":           now.Format(iso8601Format),
   217  				"X-Amz-Content-Sha256": payloadSHA256,
   218  			},
   219  			region:   region,
   220  			expected: ErrRequestNotReadyYet,
   221  		},
   222  		// (7) Should not error with invalid region instead, call should proceed
   223  		// with signature does not match.
   224  		{
   225  			queryParams: map[string]string{
   226  				"X-Amz-Algorithm":      signV4Algorithm,
   227  				"X-Amz-Date":           now.Format(iso8601Format),
   228  				"X-Amz-Expires":        "60",
   229  				"X-Amz-Signature":      "badsignature",
   230  				"X-Amz-SignedHeaders":  "host;x-amz-content-sha256;x-amz-date",
   231  				"X-Amz-Credential":     fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
   232  				"X-Amz-Content-Sha256": payloadSHA256,
   233  			},
   234  			headers: map[string]string{
   235  				"X-Amz-Date":           now.Format(iso8601Format),
   236  				"X-Amz-Content-Sha256": payloadSHA256,
   237  			},
   238  			region:   "",
   239  			expected: ErrSignatureDoesNotMatch,
   240  		},
   241  		// (8) Should error with signature does not match. But handles
   242  		// query params which do not precede with "x-amz-" header.
   243  		{
   244  			queryParams: map[string]string{
   245  				"X-Amz-Algorithm":       signV4Algorithm,
   246  				"X-Amz-Date":            now.Format(iso8601Format),
   247  				"X-Amz-Expires":         "60",
   248  				"X-Amz-Signature":       "badsignature",
   249  				"X-Amz-SignedHeaders":   "host;x-amz-content-sha256;x-amz-date",
   250  				"X-Amz-Credential":      fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
   251  				"X-Amz-Content-Sha256":  payloadSHA256,
   252  				"response-content-type": "application/json",
   253  			},
   254  			headers: map[string]string{
   255  				"X-Amz-Date":           now.Format(iso8601Format),
   256  				"X-Amz-Content-Sha256": payloadSHA256,
   257  			},
   258  			region:   "",
   259  			expected: ErrSignatureDoesNotMatch,
   260  		},
   261  		// (9) Should error with unsigned headers.
   262  		{
   263  			queryParams: map[string]string{
   264  				"X-Amz-Algorithm":       signV4Algorithm,
   265  				"X-Amz-Date":            now.Format(iso8601Format),
   266  				"X-Amz-Expires":         "60",
   267  				"X-Amz-Signature":       "badsignature",
   268  				"X-Amz-SignedHeaders":   "host;x-amz-content-sha256;x-amz-date",
   269  				"X-Amz-Credential":      fmt.Sprintf(credentialTemplate, accessKeyID, now.Format(yyyymmdd), region),
   270  				"X-Amz-Content-Sha256":  payloadSHA256,
   271  				"response-content-type": "application/json",
   272  			},
   273  			headers: map[string]string{
   274  				"X-Amz-Date": now.Format(iso8601Format),
   275  			},
   276  			region:   "",
   277  			expected: ErrUnsignedHeaders,
   278  		},
   279  	}
   280  
   281  	// Run each test case individually.
   282  	for i, testCase := range testCases {
   283  		// Turn the map[string]string into map[string][]string, because Go.
   284  		query := url.Values{}
   285  		for key, value := range testCase.queryParams {
   286  			query.Set(key, value)
   287  		}
   288  
   289  		// Create a request to use.
   290  		req, e := http.NewRequest(http.MethodGet, "http://host/a/b?"+query.Encode(), nil)
   291  		if e != nil {
   292  			t.Errorf("(%d) failed to create http.Request, got %v", i, e)
   293  		}
   294  
   295  		// Do the same for the headers.
   296  		for key, value := range testCase.headers {
   297  			req.Header.Set(key, value)
   298  		}
   299  
   300  		// parse form.
   301  		req.ParseForm()
   302  
   303  		// Check if it matches!
   304  		err := doesPresignedSignatureMatch(payloadSHA256, req, testCase.region, serviceS3)
   305  		if err != testCase.expected {
   306  			t.Errorf("(%d) expected to get %s, instead got %s", i, niceError(testCase.expected), niceError(err))
   307  		}
   308  	}
   309  }