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 }