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

     1  // Copyright (c) 2015-2023 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  	"net/http"
    23  	"os"
    24  	"testing"
    25  	"time"
    26  
    27  	"github.com/minio/madmin-go/v3"
    28  	"github.com/minio/minio/internal/auth"
    29  	xhttp "github.com/minio/minio/internal/http"
    30  )
    31  
    32  func TestCheckValid(t *testing.T) {
    33  	ctx, cancel := context.WithCancel(context.Background())
    34  	defer cancel()
    35  
    36  	objLayer, fsDir, err := prepareFS(ctx)
    37  	if err != nil {
    38  		t.Fatal(err)
    39  	}
    40  	defer os.RemoveAll(fsDir)
    41  	if err = newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
    42  		t.Fatalf("unable initialize config file, %s", err)
    43  	}
    44  
    45  	initAllSubsystems(ctx)
    46  	initConfigSubsystem(ctx, objLayer)
    47  
    48  	globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
    49  
    50  	req, err := newTestRequest(http.MethodGet, "http://example.com:9000/bucket/object", 0, nil)
    51  	if err != nil {
    52  		t.Fatal(err)
    53  	}
    54  
    55  	if err = signRequestV4(req, globalActiveCred.AccessKey, globalActiveCred.SecretKey); err != nil {
    56  		t.Fatal(err)
    57  	}
    58  
    59  	_, owner, s3Err := checkKeyValid(req, globalActiveCred.AccessKey)
    60  	if s3Err != ErrNone {
    61  		t.Fatalf("Unexpected failure with %v", errorCodes.ToAPIErr(s3Err))
    62  	}
    63  
    64  	if !owner {
    65  		t.Fatalf("Expected owner to be 'true', found %t", owner)
    66  	}
    67  
    68  	_, _, s3Err = checkKeyValid(req, "does-not-exist")
    69  	if s3Err != ErrInvalidAccessKeyID {
    70  		t.Fatalf("Expected error 'ErrInvalidAccessKeyID', found %v", s3Err)
    71  	}
    72  
    73  	ucreds, err := auth.CreateCredentials("myuser1", "mypassword1")
    74  	if err != nil {
    75  		t.Fatalf("unable create credential, %s", err)
    76  	}
    77  
    78  	globalIAMSys.CreateUser(ctx, ucreds.AccessKey, madmin.AddOrUpdateUserReq{
    79  		SecretKey: ucreds.SecretKey,
    80  		Status:    madmin.AccountEnabled,
    81  	})
    82  
    83  	_, owner, s3Err = checkKeyValid(req, ucreds.AccessKey)
    84  	if s3Err != ErrNone {
    85  		t.Fatalf("Unexpected failure with %v", errorCodes.ToAPIErr(s3Err))
    86  	}
    87  
    88  	if owner {
    89  		t.Fatalf("Expected owner to be 'false', found %t", owner)
    90  	}
    91  }
    92  
    93  // TestSkipContentSha256Cksum - Test validate the logic which decides whether
    94  // to skip checksum validation based on the request header.
    95  func TestSkipContentSha256Cksum(t *testing.T) {
    96  	testCases := []struct {
    97  		inputHeaderKey   string
    98  		inputHeaderValue string
    99  
   100  		inputQueryKey   string
   101  		inputQueryValue string
   102  
   103  		expectedResult bool
   104  	}{
   105  		// Test case - 1.
   106  		// Test case with "X-Amz-Content-Sha256" header set, but to empty value but we can't skip.
   107  		{"X-Amz-Content-Sha256", "", "", "", false},
   108  
   109  		// Test case - 2.
   110  		// Test case with "X-Amz-Content-Sha256" not set so we can skip.
   111  		{"", "", "", "", true},
   112  
   113  		// Test case - 3.
   114  		// Test case with "X-Amz-Content-Sha256" header set to  "UNSIGNED-PAYLOAD"
   115  		// When "X-Amz-Content-Sha256" header is set to  "UNSIGNED-PAYLOAD", validation of content sha256 has to be skipped.
   116  		{"X-Amz-Content-Sha256", unsignedPayload, "X-Amz-Credential", "", true},
   117  
   118  		// Test case - 4.
   119  		// Enabling PreSigned Signature v4, but X-Amz-Content-Sha256 not set has to be skipped.
   120  		{"", "", "X-Amz-Credential", "", true},
   121  
   122  		// Test case - 5.
   123  		// Enabling PreSigned Signature v4, but X-Amz-Content-Sha256 set and its not UNSIGNED-PAYLOAD, we shouldn't skip.
   124  		{"X-Amz-Content-Sha256", "somevalue", "X-Amz-Credential", "", false},
   125  
   126  		// Test case - 6.
   127  		// Test case with "X-Amz-Content-Sha256" header set to  "UNSIGNED-PAYLOAD" and its not presigned, we should skip.
   128  		{"X-Amz-Content-Sha256", unsignedPayload, "", "", true},
   129  
   130  		// Test case - 7.
   131  		// "X-Amz-Content-Sha256" not set and  PreSigned Signature v4 not enabled, sha256 checksum calculation is not skipped.
   132  		{"", "", "X-Amz-Credential", "", true},
   133  
   134  		// Test case - 8.
   135  		// "X-Amz-Content-Sha256" has a proper value cannot skip.
   136  		{"X-Amz-Content-Sha256", "somevalue", "", "", false},
   137  	}
   138  
   139  	for i, testCase := range testCases {
   140  		// creating an input HTTP request.
   141  		// Only the headers are relevant for this particular test.
   142  		inputReq, err := http.NewRequest(http.MethodGet, "http://example.com", nil)
   143  		if err != nil {
   144  			t.Fatalf("Error initializing input HTTP request: %v", err)
   145  		}
   146  		if testCase.inputQueryKey != "" {
   147  			q := inputReq.URL.Query()
   148  			q.Add(testCase.inputQueryKey, testCase.inputQueryValue)
   149  			if testCase.inputHeaderKey != "" {
   150  				q.Add(testCase.inputHeaderKey, testCase.inputHeaderValue)
   151  			}
   152  			inputReq.URL.RawQuery = q.Encode()
   153  		} else if testCase.inputHeaderKey != "" {
   154  			inputReq.Header.Set(testCase.inputHeaderKey, testCase.inputHeaderValue)
   155  		}
   156  		inputReq.ParseForm()
   157  
   158  		actualResult := skipContentSha256Cksum(inputReq)
   159  		if testCase.expectedResult != actualResult {
   160  			t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
   161  		}
   162  	}
   163  }
   164  
   165  // TestIsValidRegion - Tests validate the comparison logic for asserting whether the region from http request is valid.
   166  func TestIsValidRegion(t *testing.T) {
   167  	testCases := []struct {
   168  		inputReqRegion  string
   169  		inputConfRegion string
   170  
   171  		expectedResult bool
   172  	}{
   173  		{"", "", true},
   174  		{globalMinioDefaultRegion, "", true},
   175  		{globalMinioDefaultRegion, "US", true},
   176  		{"us-west-1", "US", false},
   177  		{"us-west-1", "us-west-1", true},
   178  		// "US" was old naming convention for 'us-east-1'.
   179  		{"US", "US", true},
   180  	}
   181  
   182  	for i, testCase := range testCases {
   183  		actualResult := isValidRegion(testCase.inputReqRegion, testCase.inputConfRegion)
   184  		if testCase.expectedResult != actualResult {
   185  			t.Errorf("Test %d: Expected the result to `%v`, but instead got `%v`", i+1, testCase.expectedResult, actualResult)
   186  		}
   187  	}
   188  }
   189  
   190  // TestExtractSignedHeaders - Tests validate extraction of signed headers using list of signed header keys.
   191  func TestExtractSignedHeaders(t *testing.T) {
   192  	signedHeaders := []string{"host", "x-amz-content-sha256", "x-amz-date", "transfer-encoding"}
   193  
   194  	// If the `expect` key exists in the signed headers then golang server would have stripped out the value, expecting the `expect` header set to `100-continue` in the result.
   195  	signedHeaders = append(signedHeaders, "expect")
   196  	// expected header values.
   197  	expectedHost := "play.min.io:9000"
   198  	expectedContentSha256 := "1234abcd"
   199  	expectedTime := UTCNow().Format(iso8601Format)
   200  	expectedTransferEncoding := "gzip"
   201  	expectedExpect := "100-continue"
   202  
   203  	r, err := http.NewRequest(http.MethodGet, "http://play.min.io:9000", nil)
   204  	if err != nil {
   205  		t.Fatal("Unable to create http.Request :", err)
   206  	}
   207  	r.TransferEncoding = []string{expectedTransferEncoding}
   208  
   209  	// Creating input http header.
   210  	inputHeader := r.Header
   211  	inputHeader.Set("x-amz-content-sha256", expectedContentSha256)
   212  	inputHeader.Set("x-amz-date", expectedTime)
   213  	// calling the function being tested.
   214  	extractedSignedHeaders, errCode := extractSignedHeaders(signedHeaders, r)
   215  	if errCode != ErrNone {
   216  		t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrNone, errCode)
   217  	}
   218  
   219  	inputQuery := r.URL.Query()
   220  	// case where some headers need to get from request query
   221  	signedHeaders = append(signedHeaders, "x-amz-server-side-encryption")
   222  	// expect to fail with `ErrUnsignedHeaders` because couldn't find some header
   223  	_, errCode = extractSignedHeaders(signedHeaders, r)
   224  	if errCode != ErrUnsignedHeaders {
   225  		t.Fatalf("Expected the APIErrorCode to %d, but got %d", ErrUnsignedHeaders, errCode)
   226  	}
   227  	// set headers value through Get parameter
   228  	inputQuery.Add("x-amz-server-side-encryption", xhttp.AmzEncryptionAES)
   229  	r.URL.RawQuery = inputQuery.Encode()
   230  	r.ParseForm()
   231  	_, errCode = extractSignedHeaders(signedHeaders, r)
   232  	if errCode != ErrNone {
   233  		t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrNone, errCode)
   234  	}
   235  
   236  	// "x-amz-content-sha256" header value from the extracted result.
   237  	extractedContentSha256 := extractedSignedHeaders.Get("x-amz-content-sha256")
   238  	// "host" header value from the extracted result.
   239  	extractedHost := extractedSignedHeaders.Get("host")
   240  	//  "x-amz-date" header from the extracted result.
   241  	extractedDate := extractedSignedHeaders.Get("x-amz-date")
   242  	// extracted `expect` header.
   243  	extractedExpect := extractedSignedHeaders.Get("expect")
   244  
   245  	extractedTransferEncoding := extractedSignedHeaders.Get("transfer-encoding")
   246  
   247  	if expectedHost != extractedHost {
   248  		t.Errorf("host header mismatch: expected `%s`, got `%s`", expectedHost, extractedHost)
   249  	}
   250  	// assert the result with the expected value.
   251  	if expectedContentSha256 != extractedContentSha256 {
   252  		t.Errorf("x-amz-content-sha256 header mismatch: expected `%s`, got `%s`", expectedContentSha256, extractedContentSha256)
   253  	}
   254  	if expectedTime != extractedDate {
   255  		t.Errorf("x-amz-date header mismatch: expected `%s`, got `%s`", expectedTime, extractedDate)
   256  	}
   257  	if extractedTransferEncoding != expectedTransferEncoding {
   258  		t.Errorf("transfer-encoding mismatch: expected %s, got %s", expectedTransferEncoding, extractedTransferEncoding)
   259  	}
   260  
   261  	// Since the list of signed headers value contained `expect`, the default value of `100-continue` will be added to extracted signed headers.
   262  	if extractedExpect != expectedExpect {
   263  		t.Errorf("expect header incorrect value: expected `%s`, got `%s`", expectedExpect, extractedExpect)
   264  	}
   265  
   266  	// case where the headers don't contain the one of the signed header in the signed headers list.
   267  	signedHeaders = append(signedHeaders, "X-Amz-Credential")
   268  	// expected to fail with `ErrUnsignedHeaders`.
   269  	_, errCode = extractSignedHeaders(signedHeaders, r)
   270  	if errCode != ErrUnsignedHeaders {
   271  		t.Fatalf("Expected the APIErrorCode to %d, but got %d", ErrUnsignedHeaders, errCode)
   272  	}
   273  
   274  	// case where the list of signed headers doesn't contain the host field.
   275  	signedHeaders = signedHeaders[2:5]
   276  	// expected to fail with `ErrUnsignedHeaders`.
   277  	_, errCode = extractSignedHeaders(signedHeaders, r)
   278  	if errCode != ErrUnsignedHeaders {
   279  		t.Fatalf("Expected the APIErrorCode to %d, but got %d", ErrUnsignedHeaders, errCode)
   280  	}
   281  }
   282  
   283  // TestSignV4TrimAll - tests the logic of TrimAll() function
   284  func TestSignV4TrimAll(t *testing.T) {
   285  	testCases := []struct {
   286  		// Input.
   287  		inputStr string
   288  		// Expected result.
   289  		result string
   290  	}{
   291  		{"本語", "本語"},
   292  		{" abc ", "abc"},
   293  		{" a b ", "a b"},
   294  		{"a b ", "a b"},
   295  		{"a  b", "a b"},
   296  		{"a   b", "a b"},
   297  		{"   a   b  c   ", "a b c"},
   298  		{"a \t b  c   ", "a b c"},
   299  		{"\"a \t b  c   ", "\"a b c"},
   300  		{" \t\n\u000b\r\fa \t\n\u000b\r\f b \t\n\u000b\r\f c \t\n\u000b\r\f", "a b c"},
   301  	}
   302  
   303  	// Tests generated values from url encoded name.
   304  	for i, testCase := range testCases {
   305  		result := signV4TrimAll(testCase.inputStr)
   306  		if testCase.result != result {
   307  			t.Errorf("Test %d: Expected signV4TrimAll result to be \"%s\", but found it to be \"%s\" instead", i+1, testCase.result, result)
   308  		}
   309  	}
   310  }
   311  
   312  // Test getContentSha256Cksum
   313  func TestGetContentSha256Cksum(t *testing.T) {
   314  	testCases := []struct {
   315  		h        string // header SHA256
   316  		q        string // query SHA256
   317  		expected string // expected SHA256
   318  	}{
   319  		{"shastring", "", "shastring"},
   320  		{emptySHA256, "", emptySHA256},
   321  		{"", "", emptySHA256},
   322  		{"", "X-Amz-Credential=random", unsignedPayload},
   323  		{"", "X-Amz-Credential=random&X-Amz-Content-Sha256=" + unsignedPayload, unsignedPayload},
   324  		{"", "X-Amz-Credential=random&X-Amz-Content-Sha256=shastring", "shastring"},
   325  	}
   326  
   327  	for i, testCase := range testCases {
   328  		r, err := http.NewRequest(http.MethodGet, "http://localhost/?"+testCase.q, nil)
   329  		if err != nil {
   330  			t.Fatal(err)
   331  		}
   332  		if testCase.h != "" {
   333  			r.Header.Set("x-amz-content-sha256", testCase.h)
   334  		}
   335  		r.ParseForm()
   336  		got := getContentSha256Cksum(r, serviceS3)
   337  		if got != testCase.expected {
   338  			t.Errorf("Test %d: got:%s expected:%s", i+1, got, testCase.expected)
   339  		}
   340  	}
   341  }
   342  
   343  // Test TestCheckMetaHeaders tests the logic of checkMetaHeaders() function
   344  func TestCheckMetaHeaders(t *testing.T) {
   345  	signedHeadersMap := map[string][]string{
   346  		"X-Amz-Meta-Test":      {"test"},
   347  		"X-Amz-Meta-Extension": {"png"},
   348  		"X-Amz-Meta-Name":      {"imagepng"},
   349  	}
   350  	expectedMetaTest := "test"
   351  	expectedMetaExtension := "png"
   352  	expectedMetaName := "imagepng"
   353  	r, err := http.NewRequest(http.MethodPut, "http://play.min.io:9000", nil)
   354  	if err != nil {
   355  		t.Fatal("Unable to create http.Request :", err)
   356  	}
   357  
   358  	// Creating input http header.
   359  	inputHeader := r.Header
   360  	inputHeader.Set("X-Amz-Meta-Test", expectedMetaTest)
   361  	inputHeader.Set("X-Amz-Meta-Extension", expectedMetaExtension)
   362  	inputHeader.Set("X-Amz-Meta-Name", expectedMetaName)
   363  	// calling the function being tested.
   364  	errCode := checkMetaHeaders(signedHeadersMap, r)
   365  	if errCode != ErrNone {
   366  		t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrNone, errCode)
   367  	}
   368  
   369  	// Add new metadata in inputHeader
   370  	inputHeader.Set("X-Amz-Meta-Clone", "fail")
   371  	// calling the function being tested.
   372  	errCode = checkMetaHeaders(signedHeadersMap, r)
   373  	if errCode != ErrUnsignedHeaders {
   374  		t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrUnsignedHeaders, errCode)
   375  	}
   376  
   377  	// Delete extra metadata from header to don't affect other test
   378  	inputHeader.Del("X-Amz-Meta-Clone")
   379  	// calling the function being tested.
   380  	errCode = checkMetaHeaders(signedHeadersMap, r)
   381  	if errCode != ErrNone {
   382  		t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrNone, errCode)
   383  	}
   384  
   385  	// Creating input url values
   386  	r, err = http.NewRequest(http.MethodPut, "http://play.min.io:9000?x-amz-meta-test=test&x-amz-meta-extension=png&x-amz-meta-name=imagepng", nil)
   387  	if err != nil {
   388  		t.Fatal("Unable to create http.Request :", err)
   389  	}
   390  
   391  	r.ParseForm()
   392  	// calling the function being tested.
   393  	errCode = checkMetaHeaders(signedHeadersMap, r)
   394  	if errCode != ErrNone {
   395  		t.Fatalf("Expected the APIErrorCode to be %d, but got %d", ErrNone, errCode)
   396  	}
   397  }