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 }