github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/gateway/sig/v4_test.go (about) 1 package sig_test 2 3 import ( 4 "bytes" 5 "context" 6 "crypto/sha256" 7 "encoding/hex" 8 "errors" 9 "fmt" 10 "io" 11 "net/http" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/aws/aws-sdk-go-v2/aws" 17 v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" 18 "github.com/treeverse/lakefs/pkg/auth/model" 19 gtwerrors "github.com/treeverse/lakefs/pkg/gateway/errors" 20 "github.com/treeverse/lakefs/pkg/gateway/sig" 21 ) 22 23 var mockCreds = &model.Credential{ 24 BaseCredential: model.BaseCredential{ 25 AccessKeyID: "AKIAIOSFODNN7EXAMPLE", 26 SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 27 }, 28 } 29 30 func TestDoesPolicySignatureMatch(t *testing.T) { 31 testCases := []struct { 32 Name string 33 Header http.Header 34 Host string 35 Method string 36 Path string 37 ExpectedParseError bool 38 ExpectedError bool 39 }{ 40 { 41 Name: "no headers", 42 Header: http.Header{}, 43 ExpectedParseError: true, 44 }, 45 { 46 Name: "missing headers", 47 Header: http.Header{ 48 "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd"}, 49 }, 50 ExpectedError: true, 51 }, 52 { 53 Name: "It should fail with a bad signature.", 54 Header: http.Header{ 55 "X-Amz-Credential": []string{"EXAMPLEINVALIDEXAMPL/20130524/us-east-1/s3/aws4_request"}, 56 "X-Amz-Date": []string{"20130524T000000Z"}, 57 "X-Amz-Signature": []string{"invalidsignature"}, 58 "Policy": []string{"policy"}, 59 }, 60 ExpectedParseError: true, 61 }, 62 { 63 Name: "Amazon single chunk example", // https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html 64 Header: http.Header{ 65 "X-Amz-Credential": []string{"AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"}, 66 "X-Amz-Date": []string{"20130524T000000Z"}, 67 "X-Amz-Content-Sha256": []string{"44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072"}, 68 "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd"}, 69 "X-Amz-Storage-Class": []string{"REDUCED_REDUNDANCY"}, 70 "Policy": []string{"policy"}, 71 72 "Date": []string{"Fri, 24 May 2013 00:00:00 GMT"}, 73 }, 74 Host: "examplebucket.s3.amazonaws.com", 75 Method: http.MethodPut, 76 Path: "test$file.text", 77 }, 78 } 79 80 for _, tc := range testCases { 81 t.Run(tc.Name, func(t *testing.T) { 82 url := fmt.Sprintf("http://%s/%s", tc.Host, tc.Path) 83 req, e := http.NewRequest(tc.Method, url, nil) 84 if e != nil { 85 t.Fatalf("failed to create http.Request, got %v", e) 86 } 87 88 // Do the same for the headers. 89 req.Header = tc.Header 90 authenticator := sig.NewV4Authenticator(req) 91 _, err := authenticator.Parse() 92 if err != nil { 93 if !tc.ExpectedParseError { 94 t.Fatal(err) 95 } 96 return 97 } 98 99 err = authenticator.Verify(mockCreds) 100 if err != nil { 101 if !tc.ExpectedError { 102 t.Fatal(err) 103 } 104 return 105 } 106 }) 107 } 108 } 109 110 func TestSingleChunkPut(t *testing.T) { 111 tt := []struct { 112 Name string 113 Host string 114 RequestBody string 115 SignBody string 116 ExpectedReadError error 117 }{ 118 { 119 Name: "amazon example", 120 RequestBody: "Welcome to Amazon S3.", 121 SignBody: "Welcome to Amazon S3.", 122 }, 123 { 124 Name: "amazon example should fail", 125 RequestBody: "Welcome to Amazon S3", 126 SignBody: "Welcome to Amazon S3.", 127 ExpectedReadError: gtwerrors.ErrSignatureDoesNotMatch, 128 }, 129 { 130 Name: "empty body", 131 RequestBody: "", 132 SignBody: "", 133 }, 134 } 135 const ( 136 testURL = "https://example.test/foo" 137 testAccessKeyID = "AKIAIOSFODNN7EXAMPLE" 138 testSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 139 ) 140 ctx := context.Background() 141 creds := aws.Credentials{ 142 AccessKeyID: testAccessKeyID, 143 SecretAccessKey: testSecretAccessKey, 144 } 145 146 for _, tc := range tt { 147 t.Run(tc.Name, func(t *testing.T) { 148 // build request with amazons sdk 149 req, err := http.NewRequest(http.MethodPut, testURL, nil) 150 if err != nil { 151 t.Fatalf("expect not no error, got %v", err) 152 } 153 154 h := sha256.Sum256([]byte(tc.SignBody)) 155 payloadHash := hex.EncodeToString(h[:]) 156 req.Header.Set("X-Amz-Content-Sha256", payloadHash) 157 158 sigTime := time.Now() 159 signer := v4.NewSigner() 160 err = signer.SignHTTP(ctx, creds, req, payloadHash, "s3", "us-east-1", sigTime) 161 if err != nil { 162 t.Fatalf("expect not no error, got %v", err) 163 } 164 165 // verify request with our authenticator 166 req.Body = io.NopCloser(strings.NewReader(tc.RequestBody)) 167 authenticator := sig.NewV4Authenticator(req) 168 _, err = authenticator.Parse() 169 if err != nil { 170 t.Fatalf("expect not no error, got %v", err) 171 } 172 173 err = authenticator.Verify(&model.Credential{ 174 BaseCredential: model.BaseCredential{ 175 AccessKeyID: testAccessKeyID, 176 SecretAccessKey: testSecretAccessKey, 177 IssuedDate: sigTime, 178 }, 179 }) 180 if err != nil { 181 t.Fatalf("expect not no error, got %v", err) 182 } 183 184 // read all 185 _, err = io.ReadAll(req.Body) 186 if !errors.Is(err, tc.ExpectedReadError) { 187 t.Errorf("expect Error %v error, got %s", tc.ExpectedReadError, err) 188 } 189 }) 190 } 191 } 192 193 func TestStreaming(t *testing.T) { 194 const ( 195 method = http.MethodPut 196 host = "s3.amazonaws.com" 197 path = "examplebucket/chunkObject.txt" 198 ID = "AKIAIOSFODNN7EXAMPLE" 199 SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 200 ) 201 req, err := http.NewRequest(method, fmt.Sprintf("https://%s/%s", host, path), nil) 202 if err != nil { 203 t.Fatal(err) 204 } 205 req.Header = http.Header{ 206 "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-storage-class,Signature=4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9"}, 207 "X-Amz-Credential": []string{"EXAMPLEINVALIDEXAMPL/20130524/us-east-1/s3/aws4_request"}, 208 "X-Amz-Date": []string{"20130524T000000Z"}, 209 "X-Amz-Storage-Class": []string{"REDUCED_REDUNDANCY"}, 210 "X-Amz-Content-Sha256": []string{"STREAMING-AWS4-HMAC-SHA256-PAYLOAD"}, 211 "Content-Encoding": []string{"aws-chunked"}, 212 "X-Amz-Decoded-Content-Length": []string{"66560"}, 213 "Content-Length": []string{"66824"}, 214 } 215 chunk1Size := 65536 216 a := bytes.Repeat([]byte("a"), chunk1Size) 217 a = append(a, '\r', '\n') 218 chunk1 := append([]byte("10000;chunk-signature=ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\r\n"), a...) 219 chunk2Size := 1024 220 b := bytes.Repeat([]byte("a"), chunk2Size) 221 b = append(b, '\r', '\n') 222 chunk2 := append([]byte("400;chunk-signature=0055627c9e194cb4542bae2aa5492e3c1575bbb81b612b7d234b86a503ef5497\r\n"), b...) 223 chunk3 := []byte("0;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\r\n\r\n") 224 body := append(chunk1, chunk2...) 225 body = append(body, chunk3...) 226 req.Body = io.NopCloser(bytes.NewReader(body)) 227 228 // now test it 229 authenticator := sig.NewV4Authenticator(req) 230 _, err = authenticator.Parse() 231 if err != nil { 232 t.Errorf("expect not no error, got %v", err) 233 } 234 235 err = authenticator.Verify(&model.Credential{ 236 BaseCredential: model.BaseCredential{ 237 AccessKeyID: ID, 238 SecretAccessKey: SECRET, 239 IssuedDate: time.Now(), 240 }, 241 }) 242 if err != nil { 243 t.Error(err) 244 } 245 if req.ContentLength != int64(chunk1Size+chunk2Size) { 246 t.Fatal("expected content length to be equal to decoded content length") 247 } 248 _, err = io.ReadAll(req.Body) 249 if err != nil { 250 t.Fatal(err) 251 } 252 } 253 254 func TestStreamingLastByteWrong(t *testing.T) { 255 const ( 256 method = http.MethodPut 257 ID = "AKIAIOSFODNN7EXAMPLE" 258 SECRET = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 259 ) 260 req, err := http.NewRequest(method, "https://s3.amazonaws.com/examplebucket/chunkObject.txt", nil) 261 if err != nil { 262 t.Fatal(err) 263 } 264 req.Header = http.Header{ 265 "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-storage-class,Signature=4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9"}, 266 "X-Amz-Credential": []string{"EXAMPLEINVALIDEXAMPL/20130524/us-east-1/s3/aws4_request"}, 267 "X-Amz-Date": []string{"20130524T000000Z"}, 268 "X-Amz-Storage-Class": []string{"REDUCED_REDUNDANCY"}, 269 "X-Amz-Content-Sha256": []string{"STREAMING-AWS4-HMAC-SHA256-PAYLOAD"}, 270 "Content-Encoding": []string{"aws-chunked"}, 271 "X-Amz-Decoded-Content-Length": []string{"66560"}, 272 "Content-Length": []string{"66824"}, 273 } 274 275 a := bytes.Repeat([]byte("a"), 65536) 276 a = append(a, '\r', '\n') 277 chunk1 := append([]byte("10000;chunk-signature=ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\r\n"), a...) 278 b := bytes.Repeat([]byte("a"), 1023) 279 b = append(b, 'b', '\r', '\n') 280 chunk2 := append([]byte("400;chunk-signature=0055627c9e194cb4542bae2aa5492e3c1575bbb81b612b7d234b86a503ef5497\r\n"), b...) 281 chunk3 := []byte("0;chunk-signature=b6c6ea8a5354eaf15b3cb7646744f4275b71ea724fed81ceb9323e279d449df9\r\n\r\n") 282 body := append(chunk1, chunk2...) 283 body = append(body, chunk3...) 284 req.Body = io.NopCloser(bytes.NewReader(body)) 285 286 // now test it 287 authenticator := sig.NewV4Authenticator(req) 288 _, err = authenticator.Parse() 289 if err != nil { 290 t.Errorf("expect not no error, got %v", err) 291 } 292 293 err = authenticator.Verify(&model.Credential{ 294 BaseCredential: model.BaseCredential{ 295 AccessKeyID: ID, 296 SecretAccessKey: SECRET, 297 IssuedDate: time.Now(), 298 }, 299 }) 300 if err != nil { 301 t.Errorf("expect not no error, got %v", err) 302 } 303 304 _, err = io.ReadAll(req.Body) 305 if err != gtwerrors.ErrSignatureDoesNotMatch { 306 t.Errorf("expect %v, got %v", gtwerrors.ErrSignatureDoesNotMatch, err) 307 } 308 } 309 310 func TestUnsignedPayload(t *testing.T) { 311 const ( 312 testID = "AKIAIOSFODNN7EXAMPLE" 313 testSecret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 314 ) 315 req, err := http.NewRequest(http.MethodHead, "https://repo1.s3.dev.lakefs.io/imdb-spark/collections/shows/title.basics.tsv.gz", nil) 316 if err != nil { 317 t.Fatal(err) 318 } 319 req.Header = http.Header{ 320 "X-Forwarded-For": []string{"10.20.1.90"}, 321 "X-Forwarded-Proto": []string{"https"}, 322 "X-Forwarded-Port": []string{"443"}, 323 "Host": []string{"repo1.s3.dev.lakefs.io"}, 324 "X-Amzn-Trace-UploadId": []string{"Root=1-5eb036bc-dd84b3a2115db68a77b1c068"}, 325 "amz-sdk-invocation-id": []string{"a8288d69-e8fa-219d-856b-b58b53b6fd5b"}, 326 "amz-sdk-retry": []string{"0/0/500"}, 327 "Authorization": []string{"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20200504/dev/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-retry;content-type;host;user-agent;x-amz-content-sha256;x-amz-date, Signature=9e54ee9b3917a632abc594f4a013cd0580331e627f60de9fffac26ba5b067b81"}, 328 "Content-Type": []string{"application/octet-stream"}, 329 "User-Agent": []string{"Hadoop 2.8.5-amzn-5, aws-sdk-java/1.11.682 Linux/4.14.154-99.181.amzn1.x86_64 OpenJDK_64-Bit_Server_VM/25.242-b08 java/1.8.0_242 scala/2.11.12 vendor/Oracle_Corporation"}, 330 "x-amz-content-sha256": []string{"UNSIGNED-PAYLOAD"}, 331 "X-Amz-Date": []string{"20200504T153732Z"}, 332 } 333 334 authenticator := sig.NewV4Authenticator(req) 335 _, err = authenticator.Parse() 336 if err != nil { 337 t.Errorf("expect not no error, got %v", err) 338 } 339 340 err = authenticator.Verify(&model.Credential{ 341 BaseCredential: model.BaseCredential{ 342 AccessKeyID: testID, 343 SecretAccessKey: testSecret, 344 IssuedDate: time.Now(), 345 }, 346 }) 347 if err != nil { 348 t.Errorf("expect not no error, got %v", err) 349 } 350 }