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  }