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

     1  /*
     2   * MinIO Cloud Storage, (C) 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
    18  
    19  import (
    20  	"net/url"
    21  	"strconv"
    22  	"strings"
    23  	"testing"
    24  	"time"
    25  )
    26  
    27  // generates credential string from its fields.
    28  func generateCredentialStr(accessKey, date, region, service, requestVersion string) string {
    29  	return "Credential=" + joinWithSlash(accessKey, date, region, service, requestVersion)
    30  }
    31  
    32  // joins the argument strings with a '/' and returns it.
    33  func joinWithSlash(accessKey, date, region, service, requestVersion string) string {
    34  	return strings.Join([]string{
    35  		accessKey,
    36  		date,
    37  		region,
    38  		service,
    39  		requestVersion}, SlashSeparator)
    40  }
    41  
    42  // generate CredentialHeader from its fields.
    43  func generateCredentials(t *testing.T, accessKey string, date string, region, service, requestVersion string) credentialHeader {
    44  	cred := credentialHeader{
    45  		accessKey: accessKey,
    46  	}
    47  	parsedDate, err := time.Parse(yyyymmdd, date)
    48  	if err != nil {
    49  		t.Fatalf("Failed to parse date")
    50  	}
    51  	cred.scope.date = parsedDate
    52  	cred.scope.region = region
    53  	cred.scope.service = service
    54  	cred.scope.request = requestVersion
    55  
    56  	return cred
    57  }
    58  
    59  // validates the credential fields against the expected credential.
    60  func validateCredentialfields(t *testing.T, testNum int, expectedCredentials credentialHeader, actualCredential credentialHeader) {
    61  
    62  	if expectedCredentials.accessKey != actualCredential.accessKey {
    63  		t.Errorf("Test %d: AccessKey mismatch: Expected \"%s\", got \"%s\"", testNum, expectedCredentials.accessKey, actualCredential.accessKey)
    64  	}
    65  	if !expectedCredentials.scope.date.Equal(actualCredential.scope.date) {
    66  		t.Errorf("Test %d: Date mismatch:Expected \"%s\", got \"%s\"", testNum, expectedCredentials.scope.date, actualCredential.scope.date)
    67  	}
    68  	if expectedCredentials.scope.region != actualCredential.scope.region {
    69  		t.Errorf("Test %d: region mismatch:Expected \"%s\", got \"%s\"", testNum, expectedCredentials.scope.region, actualCredential.scope.region)
    70  	}
    71  	if expectedCredentials.scope.service != actualCredential.scope.service {
    72  		t.Errorf("Test %d: service mismatch:Expected \"%s\", got \"%s\"", testNum, expectedCredentials.scope.service, actualCredential.scope.service)
    73  	}
    74  
    75  	if expectedCredentials.scope.request != actualCredential.scope.request {
    76  		t.Errorf("Test %d: scope request mismatch:Expected \"%s\", got \"%s\"", testNum, expectedCredentials.scope.request, actualCredential.scope.request)
    77  	}
    78  }
    79  
    80  // TestParseCredentialHeader - validates the format validator and extractor for the Credential header in an aws v4 request.
    81  // A valid format of creadential should be of the following format.
    82  // Credential = accessKey + SlashSeparator+ scope
    83  // where scope = string.Join([]string{  currTime.Format(yyyymmdd),
    84  // 			globalMinioDefaultRegion,
    85  //               	"s3",
    86  //		        "aws4_request",
    87  //                       },SlashSeparator)
    88  func TestParseCredentialHeader(t *testing.T) {
    89  
    90  	sampleTimeStr := UTCNow().Format(yyyymmdd)
    91  
    92  	testCases := []struct {
    93  		inputCredentialStr  string
    94  		expectedCredentials credentialHeader
    95  		expectedErrCode     APIErrorCode
    96  	}{
    97  		// Test Case - 1.
    98  		// Test case with no '=' in te inputCredentialStr.
    99  		{
   100  			inputCredentialStr:  "Credential",
   101  			expectedCredentials: credentialHeader{},
   102  			expectedErrCode:     ErrMissingFields,
   103  		},
   104  		// Test Case - 2.
   105  		// Test case with no "Credential" string in te inputCredentialStr.
   106  		{
   107  			inputCredentialStr:  "Cred=",
   108  			expectedCredentials: credentialHeader{},
   109  			expectedErrCode:     ErrMissingCredTag,
   110  		},
   111  		// Test Case - 3.
   112  		// Test case with malformed credentials.
   113  		{
   114  			inputCredentialStr:  "Credential=abc",
   115  			expectedCredentials: credentialHeader{},
   116  			expectedErrCode:     ErrCredMalformed,
   117  		},
   118  		// Test Case - 4.
   119  		// Test case with AccessKey of length 2.
   120  		{
   121  			inputCredentialStr: generateCredentialStr(
   122  				"^#",
   123  				UTCNow().Format(yyyymmdd),
   124  				"ABCD",
   125  				"ABCD",
   126  				"ABCD"),
   127  			expectedCredentials: credentialHeader{},
   128  			expectedErrCode:     ErrInvalidAccessKeyID,
   129  		},
   130  		// Test Case - 5.
   131  		// Test case with invalid date format date.
   132  		// a valid date format for credentials is "yyyymmdd".
   133  		{
   134  			inputCredentialStr: generateCredentialStr(
   135  				"Z7IXGOO6BZ0REAN1Q26I",
   136  				UTCNow().String(),
   137  				"ABCD",
   138  				"ABCD",
   139  				"ABCD"),
   140  			expectedCredentials: credentialHeader{},
   141  			expectedErrCode:     ErrMalformedCredentialDate,
   142  		},
   143  		// Test Case - 6.
   144  		// Test case with invalid service.
   145  		// "s3" is the valid service string.
   146  		{
   147  			inputCredentialStr: generateCredentialStr(
   148  				"Z7IXGOO6BZ0REAN1Q26I",
   149  				UTCNow().Format(yyyymmdd),
   150  				"us-west-1",
   151  				"ABCD",
   152  				"ABCD"),
   153  			expectedCredentials: credentialHeader{},
   154  			expectedErrCode:     ErrInvalidServiceS3,
   155  		},
   156  		// Test Case - 7.
   157  		// Test case with invalid region.
   158  		{
   159  			inputCredentialStr: generateCredentialStr(
   160  				"Z7IXGOO6BZ0REAN1Q26I",
   161  				UTCNow().Format(yyyymmdd),
   162  				"us-west-2",
   163  				"s3",
   164  				"aws4_request"),
   165  			expectedCredentials: credentialHeader{},
   166  			expectedErrCode:     ErrAuthorizationHeaderMalformed,
   167  		},
   168  		// Test Case - 8.
   169  		// Test case with invalid request version.
   170  		// "aws4_request" is the valid request version.
   171  		{
   172  			inputCredentialStr: generateCredentialStr(
   173  				"Z7IXGOO6BZ0REAN1Q26I",
   174  				UTCNow().Format(yyyymmdd),
   175  				"us-west-1",
   176  				"s3",
   177  				"ABCD"),
   178  			expectedCredentials: credentialHeader{},
   179  			expectedErrCode:     ErrInvalidRequestVersion,
   180  		},
   181  		// Test Case - 9.
   182  		// Test case with right inputs. Expected to return a valid CredentialHeader.
   183  		// "aws4_request" is the valid request version.
   184  		{
   185  			inputCredentialStr: generateCredentialStr(
   186  				"Z7IXGOO6BZ0REAN1Q26I",
   187  				sampleTimeStr,
   188  				"us-west-1",
   189  				"s3",
   190  				"aws4_request"),
   191  			expectedCredentials: generateCredentials(
   192  				t,
   193  				"Z7IXGOO6BZ0REAN1Q26I",
   194  				sampleTimeStr,
   195  				"us-west-1",
   196  				"s3",
   197  				"aws4_request"),
   198  			expectedErrCode: ErrNone,
   199  		},
   200  		// Test Case - 10.
   201  		// Test case with right inputs -> AccessKey contains `/`. See minio/#6443
   202  		// "aws4_request" is the valid request version.
   203  		{
   204  			inputCredentialStr: generateCredentialStr(
   205  				"LOCALKEY/DEV/1",
   206  				sampleTimeStr,
   207  				"us-west-1",
   208  				"s3",
   209  				"aws4_request"),
   210  			expectedCredentials: generateCredentials(
   211  				t,
   212  				"LOCALKEY/DEV/1",
   213  				sampleTimeStr,
   214  				"us-west-1",
   215  				"s3",
   216  				"aws4_request"),
   217  			expectedErrCode: ErrNone,
   218  		},
   219  		// Test Case - 11.
   220  		// Test case with right inputs -> AccessKey contains `=`. See minio/#7376
   221  		// "aws4_request" is the valid request version.
   222  		{
   223  			inputCredentialStr: generateCredentialStr(
   224  				"LOCALKEY/DEV/1=",
   225  				sampleTimeStr,
   226  				"us-west-1",
   227  				"s3",
   228  				"aws4_request"),
   229  			expectedCredentials: generateCredentials(
   230  				t,
   231  				"LOCALKEY/DEV/1=",
   232  				sampleTimeStr,
   233  				"us-west-1",
   234  				"s3",
   235  				"aws4_request"),
   236  			expectedErrCode: ErrNone,
   237  		},
   238  	}
   239  
   240  	for i, testCase := range testCases {
   241  		actualCredential, actualErrCode := parseCredentialHeader(testCase.inputCredentialStr, "us-west-1", "s3")
   242  		// validating the credential fields.
   243  		if testCase.expectedErrCode != actualErrCode {
   244  			t.Fatalf("Test %d: Expected the APIErrCode to be %s, got %s", i+1, errorCodes[testCase.expectedErrCode].Code, errorCodes[actualErrCode].Code)
   245  		}
   246  		if actualErrCode == ErrNone {
   247  			validateCredentialfields(t, i+1, testCase.expectedCredentials, actualCredential)
   248  		}
   249  	}
   250  }
   251  
   252  // TestParseSignature - validates the logic for extracting the signature string.
   253  func TestParseSignature(t *testing.T) {
   254  	testCases := []struct {
   255  		inputSignElement string
   256  		expectedSignStr  string
   257  		expectedErrCode  APIErrorCode
   258  	}{
   259  		// Test case - 1.
   260  		// SignElemenet doesn't have 2 parts on an attempt to split at '='.
   261  		// ErrMissingFields expected.
   262  		{
   263  			inputSignElement: "Signature",
   264  			expectedSignStr:  "",
   265  			expectedErrCode:  ErrMissingFields,
   266  		},
   267  		// Test case - 2.
   268  		// SignElement does have 2 parts but doesn't have valid signature value.
   269  		// ErrMissingFields expected.
   270  		{
   271  			inputSignElement: "Signature=",
   272  			expectedSignStr:  "",
   273  			expectedErrCode:  ErrMissingFields,
   274  		},
   275  		// Test case - 3.
   276  		// SignElemenet with missing "SignatureTag",ErrMissingSignTag expected.
   277  		{
   278  			inputSignElement: "Sign=",
   279  			expectedSignStr:  "",
   280  			expectedErrCode:  ErrMissingSignTag,
   281  		},
   282  		// Test case - 4.
   283  		// Test case with valid inputs.
   284  		{
   285  			inputSignElement: "Signature=abcd",
   286  			expectedSignStr:  "abcd",
   287  			expectedErrCode:  ErrNone,
   288  		},
   289  	}
   290  	for i, testCase := range testCases {
   291  		actualSignStr, actualErrCode := parseSignature(testCase.inputSignElement)
   292  		if testCase.expectedErrCode != actualErrCode {
   293  			t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode)
   294  		}
   295  		if actualErrCode == ErrNone {
   296  			if testCase.expectedSignStr != actualSignStr {
   297  				t.Errorf("Test %d: Expected the result to be \"%s\", but got \"%s\". ", i+1, testCase.expectedSignStr, actualSignStr)
   298  
   299  			}
   300  		}
   301  
   302  	}
   303  }
   304  
   305  // TestParseSignedHeaders - validates the logic for extracting the signature string.
   306  func TestParseSignedHeaders(t *testing.T) {
   307  	testCases := []struct {
   308  		inputSignElement      string
   309  		expectedSignedHeaders []string
   310  		expectedErrCode       APIErrorCode
   311  	}{
   312  		// Test case - 1.
   313  		// SignElemenet doesn't have 2 parts on an attempt to split at '='.
   314  		// ErrMissingFields expected.
   315  		{
   316  			inputSignElement:      "SignedHeaders",
   317  			expectedSignedHeaders: nil,
   318  			expectedErrCode:       ErrMissingFields,
   319  		},
   320  		// Test case - 2.
   321  		// SignElemenet with missing "SigHeaderTag",ErrMissingSignHeadersTag expected.
   322  		{
   323  			inputSignElement:      "Sign=",
   324  			expectedSignedHeaders: nil,
   325  			expectedErrCode:       ErrMissingSignHeadersTag,
   326  		},
   327  		// Test case - 3.
   328  		// Test case with valid inputs.
   329  		{
   330  			inputSignElement:      "SignedHeaders=host;x-amz-content-sha256;x-amz-date",
   331  			expectedSignedHeaders: []string{"host", "x-amz-content-sha256", "x-amz-date"},
   332  			expectedErrCode:       ErrNone,
   333  		},
   334  	}
   335  
   336  	for i, testCase := range testCases {
   337  		actualSignedHeaders, actualErrCode := parseSignedHeader(testCase.inputSignElement)
   338  		if testCase.expectedErrCode != actualErrCode {
   339  			t.Errorf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode)
   340  		}
   341  		if actualErrCode == ErrNone {
   342  			if strings.Join(testCase.expectedSignedHeaders, ",") != strings.Join(actualSignedHeaders, ",") {
   343  				t.Errorf("Test %d: Expected the result to be \"%v\", but got \"%v\". ", i+1, testCase.expectedSignedHeaders, actualSignedHeaders)
   344  
   345  			}
   346  		}
   347  
   348  	}
   349  }
   350  
   351  // TestParseSignV4 - Tests Parsing of v4 signature form the authorization string.
   352  func TestParseSignV4(t *testing.T) {
   353  	sampleTimeStr := UTCNow().Format(yyyymmdd)
   354  	testCases := []struct {
   355  		inputV4AuthStr    string
   356  		expectedAuthField signValues
   357  		expectedErrCode   APIErrorCode
   358  	}{
   359  		// Test case - 1.
   360  		// Test case with empty auth string.
   361  		{
   362  			inputV4AuthStr:    "",
   363  			expectedAuthField: signValues{},
   364  			expectedErrCode:   ErrAuthHeaderEmpty,
   365  		},
   366  		// Test case - 2.
   367  		// Test case with no sign v4 Algorithm prefix.
   368  		// A valid authorization string should begin(prefix)
   369  		{
   370  			inputV4AuthStr:    "no-singv4AlgorithmPrefix",
   371  			expectedAuthField: signValues{},
   372  			expectedErrCode:   ErrSignatureVersionNotSupported,
   373  		},
   374  		// Test case - 3.
   375  		// Test case with missing fields.
   376  		// A valid authorization string should have 3 fields.
   377  		{
   378  			inputV4AuthStr:    signV4Algorithm,
   379  			expectedAuthField: signValues{},
   380  			expectedErrCode:   ErrMissingFields,
   381  		},
   382  		// Test case - 4.
   383  		// Test case with invalid credential field.
   384  		{
   385  			inputV4AuthStr:    signV4Algorithm + " Cred=,a,b",
   386  			expectedAuthField: signValues{},
   387  			expectedErrCode:   ErrMissingCredTag,
   388  		},
   389  		// Test case - 5.
   390  		// Auth field with missing "SigHeaderTag",ErrMissingSignHeadersTag expected.
   391  		// A vaild credential is generated.
   392  		// Test case with invalid credential field.
   393  		{
   394  			inputV4AuthStr: signV4Algorithm +
   395  				strings.Join([]string{
   396  					// generating a valid credential field.
   397  					generateCredentialStr(
   398  						"Z7IXGOO6BZ0REAN1Q26I",
   399  						sampleTimeStr,
   400  						"us-west-1",
   401  						"s3",
   402  						"aws4_request"),
   403  					// Incorrect SignedHeader field.
   404  					"SignIncorrectHeader=",
   405  					"b",
   406  				}, ","),
   407  
   408  			expectedAuthField: signValues{},
   409  			expectedErrCode:   ErrMissingSignHeadersTag,
   410  		},
   411  		// Test case - 6.
   412  		// Auth string with missing "SignatureTag",ErrMissingSignTag expected.
   413  		// A vaild credential is generated.
   414  		// Test case with invalid credential field.
   415  		{
   416  			inputV4AuthStr: signV4Algorithm +
   417  				strings.Join([]string{
   418  					// generating a valid credential.
   419  					generateCredentialStr(
   420  						"Z7IXGOO6BZ0REAN1Q26I",
   421  						sampleTimeStr,
   422  						"us-west-1",
   423  						"s3",
   424  						"aws4_request"),
   425  					// valid SignedHeader.
   426  					"SignedHeaders=host;x-amz-content-sha256;x-amz-date",
   427  					// invalid Signature field.
   428  					// a valid signature is of form "Signature="
   429  					"Sign=",
   430  				}, ","),
   431  
   432  			expectedAuthField: signValues{},
   433  			expectedErrCode:   ErrMissingSignTag,
   434  		},
   435  		// Test case - 7.
   436  		{
   437  			inputV4AuthStr: signV4Algorithm +
   438  				strings.Join([]string{
   439  					// generating a valid credential.
   440  					generateCredentialStr(
   441  						"Z7IXGOO6BZ0REAN1Q26I",
   442  						sampleTimeStr,
   443  						"us-west-1",
   444  						"s3",
   445  						"aws4_request"),
   446  					// valid SignedHeader.
   447  					"SignedHeaders=host;x-amz-content-sha256;x-amz-date",
   448  					// valid Signature field.
   449  					// a valid signature is of form "Signature="
   450  					"Signature=abcd",
   451  				}, ","),
   452  			expectedAuthField: signValues{
   453  				Credential: generateCredentials(
   454  					t,
   455  					"Z7IXGOO6BZ0REAN1Q26I",
   456  					sampleTimeStr,
   457  					"us-west-1",
   458  					"s3",
   459  					"aws4_request"),
   460  				SignedHeaders: []string{"host", "x-amz-content-sha256", "x-amz-date"},
   461  				Signature:     "abcd",
   462  			},
   463  			expectedErrCode: ErrNone,
   464  		},
   465  		// Test case - 8.
   466  		{
   467  			inputV4AuthStr: signV4Algorithm +
   468  				strings.Join([]string{
   469  					// generating a valid credential.
   470  					generateCredentialStr(
   471  						"access key",
   472  						sampleTimeStr,
   473  						"us-west-1",
   474  						"s3",
   475  						"aws4_request"),
   476  					// valid SignedHeader.
   477  					"SignedHeaders=host;x-amz-content-sha256;x-amz-date",
   478  					// valid Signature field.
   479  					// a valid signature is of form "Signature="
   480  					"Signature=abcd",
   481  				}, ","),
   482  			expectedAuthField: signValues{
   483  				Credential: generateCredentials(
   484  					t,
   485  					"access key",
   486  					sampleTimeStr,
   487  					"us-west-1",
   488  					"s3",
   489  					"aws4_request"),
   490  				SignedHeaders: []string{"host", "x-amz-content-sha256", "x-amz-date"},
   491  				Signature:     "abcd",
   492  			},
   493  			expectedErrCode: ErrNone,
   494  		},
   495  	}
   496  
   497  	for i, testCase := range testCases {
   498  		parsedAuthField, actualErrCode := parseSignV4(testCase.inputV4AuthStr, "", "s3")
   499  
   500  		if testCase.expectedErrCode != actualErrCode {
   501  			t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode)
   502  		}
   503  
   504  		if actualErrCode == ErrNone {
   505  			// validating the extracted/parsed credential fields.
   506  			validateCredentialfields(t, i+1, testCase.expectedAuthField.Credential, parsedAuthField.Credential)
   507  
   508  			// validating the extraction/parsing of signature field.
   509  			if !compareSignatureV4(testCase.expectedAuthField.Signature, parsedAuthField.Signature) {
   510  				t.Errorf("Test %d: Parsed Signature field mismatch: Expected \"%s\", got \"%s\"", i+1, testCase.expectedAuthField.Signature, parsedAuthField.Signature)
   511  			}
   512  
   513  			// validating the extracted signed headers.
   514  			if strings.Join(testCase.expectedAuthField.SignedHeaders, ",") != strings.Join(parsedAuthField.SignedHeaders, ",") {
   515  				t.Errorf("Test %d: Expected the result to be \"%v\", but got \"%v\". ", i+1, testCase.expectedAuthField, parsedAuthField.SignedHeaders)
   516  			}
   517  		}
   518  
   519  	}
   520  
   521  }
   522  
   523  // TestDoesV4PresignParamsExist - tests validate the logic to
   524  func TestDoesV4PresignParamsExist(t *testing.T) {
   525  	testCases := []struct {
   526  		inputQueryKeyVals []string
   527  		expectedErrCode   APIErrorCode
   528  	}{
   529  		// Test case - 1.
   530  		// contains all query param keys which are necessary for v4 presign request.
   531  		{
   532  			inputQueryKeyVals: []string{
   533  				"X-Amz-Algorithm", "",
   534  				"X-Amz-Credential", "",
   535  				"X-Amz-Signature", "",
   536  				"X-Amz-Date", "",
   537  				"X-Amz-SignedHeaders", "",
   538  				"X-Amz-Expires", "",
   539  			},
   540  			expectedErrCode: ErrNone,
   541  		},
   542  		// Test case - 2.
   543  		// missing 	"X-Amz-Algorithm" in tdhe query param.
   544  		// contains all query param keys which are necessary for v4 presign request.
   545  		{
   546  			inputQueryKeyVals: []string{
   547  				"X-Amz-Credential", "",
   548  				"X-Amz-Signature", "",
   549  				"X-Amz-Date", "",
   550  				"X-Amz-SignedHeaders", "",
   551  				"X-Amz-Expires", "",
   552  			},
   553  			expectedErrCode: ErrInvalidQueryParams,
   554  		},
   555  		// Test case - 3.
   556  		// missing "X-Amz-Credential" in the query param.
   557  		{
   558  			inputQueryKeyVals: []string{
   559  				"X-Amz-Algorithm", "",
   560  				"X-Amz-Signature", "",
   561  				"X-Amz-Date", "",
   562  				"X-Amz-SignedHeaders", "",
   563  				"X-Amz-Expires", "",
   564  			},
   565  			expectedErrCode: ErrInvalidQueryParams,
   566  		},
   567  		// Test case - 4.
   568  		// missing "X-Amz-Signature" in the query param.
   569  		{
   570  			inputQueryKeyVals: []string{
   571  				"X-Amz-Algorithm", "",
   572  				"X-Amz-Credential", "",
   573  				"X-Amz-Date", "",
   574  				"X-Amz-SignedHeaders", "",
   575  				"X-Amz-Expires", "",
   576  			},
   577  			expectedErrCode: ErrInvalidQueryParams,
   578  		},
   579  		// Test case - 5.
   580  		// missing "X-Amz-Date" in the query param.
   581  		{
   582  			inputQueryKeyVals: []string{
   583  				"X-Amz-Algorithm", "",
   584  				"X-Amz-Credential", "",
   585  				"X-Amz-Signature", "",
   586  				"X-Amz-SignedHeaders", "",
   587  				"X-Amz-Expires", "",
   588  			},
   589  			expectedErrCode: ErrInvalidQueryParams,
   590  		},
   591  		// Test case - 6.
   592  		// missing "X-Amz-SignedHeaders" in the query param.
   593  		{
   594  			inputQueryKeyVals: []string{
   595  				"X-Amz-Algorithm", "",
   596  				"X-Amz-Credential", "",
   597  				"X-Amz-Signature", "",
   598  				"X-Amz-Date", "",
   599  				"X-Amz-Expires", "",
   600  			},
   601  			expectedErrCode: ErrInvalidQueryParams,
   602  		},
   603  		// Test case - 7.
   604  		// missing "X-Amz-Expires" in the query param.
   605  		{
   606  			inputQueryKeyVals: []string{
   607  				"X-Amz-Algorithm", "",
   608  				"X-Amz-Credential", "",
   609  				"X-Amz-Signature", "",
   610  				"X-Amz-Date", "",
   611  				"X-Amz-SignedHeaders", "",
   612  			},
   613  			expectedErrCode: ErrInvalidQueryParams,
   614  		},
   615  	}
   616  
   617  	for i, testCase := range testCases {
   618  		inputQuery := url.Values{}
   619  		// iterating through input query key value and setting the inputQuery of type url.Values.
   620  		for j := 0; j < len(testCase.inputQueryKeyVals)-1; j += 2 {
   621  
   622  			inputQuery.Set(testCase.inputQueryKeyVals[j], testCase.inputQueryKeyVals[j+1])
   623  		}
   624  
   625  		actualErrCode := doesV4PresignParamsExist(inputQuery)
   626  
   627  		if testCase.expectedErrCode != actualErrCode {
   628  			t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode)
   629  		}
   630  	}
   631  
   632  }
   633  
   634  // TestParsePreSignV4 - Validates the parsing logic of Presignied v4 request from its url query values.
   635  func TestParsePreSignV4(t *testing.T) {
   636  	// converts the duration in seconds into string format.
   637  	getDurationStr := func(expires int) string {
   638  		return strconv.Itoa(expires)
   639  	}
   640  	// used in expected preSignValues, preSignValues.Date is of type time.Time .
   641  	queryTime := UTCNow()
   642  
   643  	sampleTimeStr := UTCNow().Format(yyyymmdd)
   644  
   645  	testCases := []struct {
   646  		inputQueryKeyVals     []string
   647  		expectedPreSignValues preSignValues
   648  		expectedErrCode       APIErrorCode
   649  	}{
   650  		// Test case - 1.
   651  		// A Valid v4 presign URL requires the following params to be in the query.
   652  		// "X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Signature", " X-Amz-Date", "X-Amz-SignedHeaders", "X-Amz-Expires".
   653  		// If these params are missing its expected to get ErrInvalidQueryParams .
   654  		// In the following test case 2 out of 6 query params are missing.
   655  		{
   656  			inputQueryKeyVals: []string{
   657  				"X-Amz-Algorithm", "",
   658  				"X-Amz-Credential", "",
   659  				"X-Amz-Signature", "",
   660  				"X-Amz-Expires", "",
   661  			},
   662  			expectedPreSignValues: preSignValues{},
   663  			expectedErrCode:       ErrInvalidQueryParams,
   664  		},
   665  		// Test case - 2.
   666  		// Test case with invalid  "X-Amz-Algorithm" query value.
   667  		// The other query params should exist, other wise ErrInvalidQueryParams will be returned because of missing fields.
   668  		{
   669  			inputQueryKeyVals: []string{
   670  
   671  				"X-Amz-Algorithm", "InvalidValue",
   672  				"X-Amz-Credential", "",
   673  				"X-Amz-Signature", "",
   674  				"X-Amz-Date", "",
   675  				"X-Amz-SignedHeaders", "",
   676  				"X-Amz-Expires", "",
   677  			},
   678  			expectedPreSignValues: preSignValues{},
   679  			expectedErrCode:       ErrInvalidQuerySignatureAlgo,
   680  		},
   681  		// Test case - 3.
   682  		// Test case with valid "X-Amz-Algorithm" query value, but invalid  "X-Amz-Credential" header.
   683  		// Malformed crenential.
   684  		{
   685  			inputQueryKeyVals: []string{
   686  				// valid  "X-Amz-Algorithm" header.
   687  				"X-Amz-Algorithm", signV4Algorithm,
   688  				// valid  "X-Amz-Credential" header.
   689  				"X-Amz-Credential", "invalid-credential",
   690  				"X-Amz-Signature", "",
   691  				"X-Amz-Date", "",
   692  				"X-Amz-SignedHeaders", "",
   693  				"X-Amz-Expires", "",
   694  			},
   695  			expectedPreSignValues: preSignValues{},
   696  			expectedErrCode:       ErrCredMalformed,
   697  		},
   698  
   699  		// Test case - 4.
   700  		// Test case with valid "X-Amz-Algorithm" query value.
   701  		// Malformed date.
   702  		{
   703  			inputQueryKeyVals: []string{
   704  				// valid  "X-Amz-Algorithm" header.
   705  				"X-Amz-Algorithm", signV4Algorithm,
   706  				// valid  "X-Amz-Credential" header.
   707  				"X-Amz-Credential", joinWithSlash(
   708  					"Z7IXGOO6BZ0REAN1Q26I",
   709  					sampleTimeStr,
   710  					"us-west-1",
   711  					"s3",
   712  					"aws4_request"),
   713  				// invalid "X-Amz-Date" query.
   714  				"X-Amz-Date", "invalid-time",
   715  				"X-Amz-SignedHeaders", "",
   716  				"X-Amz-Expires", "",
   717  				"X-Amz-Signature", "",
   718  			},
   719  			expectedPreSignValues: preSignValues{},
   720  			expectedErrCode:       ErrMalformedPresignedDate,
   721  		},
   722  		// Test case - 5.
   723  		// Test case with valid "X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date" query value.
   724  		// Malformed Expiry, a valid expiry should be of format "<int>s".
   725  		{
   726  			inputQueryKeyVals: []string{
   727  				// valid  "X-Amz-Algorithm" header.
   728  				"X-Amz-Algorithm", signV4Algorithm,
   729  				// valid  "X-Amz-Credential" header.
   730  				"X-Amz-Credential", joinWithSlash(
   731  					"Z7IXGOO6BZ0REAN1Q26I",
   732  					sampleTimeStr,
   733  					"us-west-1",
   734  					"s3",
   735  					"aws4_request"),
   736  				// valid "X-Amz-Date" query.
   737  				"X-Amz-Date", UTCNow().Format(iso8601Format),
   738  				"X-Amz-Expires", "MalformedExpiry",
   739  				"X-Amz-SignedHeaders", "",
   740  				"X-Amz-Signature", "",
   741  			},
   742  			expectedPreSignValues: preSignValues{},
   743  			expectedErrCode:       ErrMalformedExpires,
   744  		},
   745  		// Test case - 6.
   746  		// Test case with negative X-Amz-Expires header.
   747  		{
   748  			inputQueryKeyVals: []string{
   749  				// valid  "X-Amz-Algorithm" header.
   750  				"X-Amz-Algorithm", signV4Algorithm,
   751  				// valid  "X-Amz-Credential" header.
   752  				"X-Amz-Credential", joinWithSlash(
   753  					"Z7IXGOO6BZ0REAN1Q26I",
   754  					sampleTimeStr,
   755  					"us-west-1",
   756  					"s3",
   757  					"aws4_request"),
   758  				// valid "X-Amz-Date" query.
   759  				"X-Amz-Date", queryTime.UTC().Format(iso8601Format),
   760  				"X-Amz-Expires", getDurationStr(-1),
   761  				"X-Amz-Signature", "abcd",
   762  				"X-Amz-SignedHeaders", "host;x-amz-content-sha256;x-amz-date",
   763  			},
   764  			expectedPreSignValues: preSignValues{},
   765  			expectedErrCode:       ErrNegativeExpires,
   766  		},
   767  		// Test case - 7.
   768  		// Test case with empty X-Amz-SignedHeaders.
   769  		{
   770  			inputQueryKeyVals: []string{
   771  				// valid  "X-Amz-Algorithm" header.
   772  				"X-Amz-Algorithm", signV4Algorithm,
   773  				// valid  "X-Amz-Credential" header.
   774  				"X-Amz-Credential", joinWithSlash(
   775  					"Z7IXGOO6BZ0REAN1Q26I",
   776  					sampleTimeStr,
   777  					"us-west-1",
   778  					"s3",
   779  					"aws4_request"),
   780  				// valid "X-Amz-Date" query.
   781  				"X-Amz-Date", queryTime.UTC().Format(iso8601Format),
   782  				"X-Amz-Expires", getDurationStr(100),
   783  				"X-Amz-Signature", "abcd",
   784  				"X-Amz-SignedHeaders", "",
   785  			},
   786  			expectedPreSignValues: preSignValues{},
   787  			expectedErrCode:       ErrMissingFields,
   788  		},
   789  		// Test case - 8.
   790  		// Test case with valid "X-Amz-Algorithm", "X-Amz-Credential", "X-Amz-Date" query value.
   791  		// Malformed Expiry, a valid expiry should be of format "<int>s".
   792  		{
   793  			inputQueryKeyVals: []string{
   794  				// valid  "X-Amz-Algorithm" header.
   795  				"X-Amz-Algorithm", signV4Algorithm,
   796  				// valid  "X-Amz-Credential" header.
   797  				"X-Amz-Credential", joinWithSlash(
   798  					"Z7IXGOO6BZ0REAN1Q26I",
   799  					sampleTimeStr,
   800  					"us-west-1",
   801  					"s3",
   802  					"aws4_request"),
   803  				// valid "X-Amz-Date" query.
   804  				"X-Amz-Date", queryTime.UTC().Format(iso8601Format),
   805  				"X-Amz-Expires", getDurationStr(100),
   806  				"X-Amz-Signature", "abcd",
   807  				"X-Amz-SignedHeaders", "host;x-amz-content-sha256;x-amz-date",
   808  			},
   809  			expectedPreSignValues: preSignValues{
   810  				signValues{
   811  					// Credentials.
   812  					generateCredentials(
   813  						t,
   814  						"Z7IXGOO6BZ0REAN1Q26I",
   815  						sampleTimeStr,
   816  						"us-west-1",
   817  						"s3",
   818  						"aws4_request",
   819  					),
   820  					// SignedHeaders.
   821  					[]string{"host", "x-amz-content-sha256", "x-amz-date"},
   822  					// Signature.
   823  					"abcd",
   824  				},
   825  				// Date
   826  				queryTime,
   827  				// Expires.
   828  				100 * time.Second,
   829  			},
   830  			expectedErrCode: ErrNone,
   831  		},
   832  
   833  		// Test case - 9.
   834  		// Test case with value greater than 604800 in X-Amz-Expires header.
   835  		{
   836  			inputQueryKeyVals: []string{
   837  				// valid  "X-Amz-Algorithm" header.
   838  				"X-Amz-Algorithm", signV4Algorithm,
   839  				// valid  "X-Amz-Credential" header.
   840  				"X-Amz-Credential", joinWithSlash(
   841  					"Z7IXGOO6BZ0REAN1Q26I",
   842  					sampleTimeStr,
   843  					"us-west-1",
   844  					"s3",
   845  					"aws4_request"),
   846  				// valid "X-Amz-Date" query.
   847  				"X-Amz-Date", queryTime.UTC().Format(iso8601Format),
   848  				// Invalid Expiry time greater than 7 days (604800 in seconds).
   849  				"X-Amz-Expires", getDurationStr(605000),
   850  				"X-Amz-Signature", "abcd",
   851  				"X-Amz-SignedHeaders", "host;x-amz-content-sha256;x-amz-date",
   852  			},
   853  			expectedPreSignValues: preSignValues{},
   854  			expectedErrCode:       ErrMaximumExpires,
   855  		},
   856  	}
   857  
   858  	for i, testCase := range testCases {
   859  		inputQuery := url.Values{}
   860  		// iterating through input query key value and setting the inputQuery of type url.Values.
   861  		for j := 0; j < len(testCase.inputQueryKeyVals)-1; j += 2 {
   862  			inputQuery.Set(testCase.inputQueryKeyVals[j], testCase.inputQueryKeyVals[j+1])
   863  		}
   864  		// call the function under test.
   865  		parsedPreSign, actualErrCode := parsePreSignV4(inputQuery, "", serviceS3)
   866  		if testCase.expectedErrCode != actualErrCode {
   867  			t.Fatalf("Test %d: Expected the APIErrCode to be %d, got %d", i+1, testCase.expectedErrCode, actualErrCode)
   868  		}
   869  		if actualErrCode == ErrNone {
   870  			// validating credentials.
   871  			validateCredentialfields(t, i+1, testCase.expectedPreSignValues.Credential, parsedPreSign.Credential)
   872  			// validating signed headers.
   873  			if strings.Join(testCase.expectedPreSignValues.SignedHeaders, ",") != strings.Join(parsedPreSign.SignedHeaders, ",") {
   874  				t.Errorf("Test %d: Expected the result to be \"%v\", but got \"%v\". ", i+1, testCase.expectedPreSignValues.SignedHeaders, parsedPreSign.SignedHeaders)
   875  			}
   876  			// validating signature field.
   877  			if !compareSignatureV4(testCase.expectedPreSignValues.Signature, parsedPreSign.Signature) {
   878  				t.Errorf("Test %d: Signature field mismatch: Expected \"%s\", got \"%s\"", i+1, testCase.expectedPreSignValues.Signature, parsedPreSign.Signature)
   879  			}
   880  			// validating expiry duration.
   881  			if testCase.expectedPreSignValues.Expires != parsedPreSign.Expires {
   882  				t.Errorf("Test %d: Expected expiry time to be %v, but got %v", i+1, testCase.expectedPreSignValues.Expires, parsedPreSign.Expires)
   883  			}
   884  			// validating presign date field.
   885  			if testCase.expectedPreSignValues.Date.UTC().Format(iso8601Format) != parsedPreSign.Date.UTC().Format(iso8601Format) {
   886  				t.Errorf("Test %d: Expected date to be %v, but got %v", i+1, testCase.expectedPreSignValues.Date.UTC().Format(iso8601Format), parsedPreSign.Date.UTC().Format(iso8601Format))
   887  			}
   888  		}
   889  
   890  	}
   891  }