github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/gateway/sig/sig_test.go (about)

     1  package sig_test
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"math/rand"
     7  	"net/http"
     8  	"net/url"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/minio/minio-go/v7/pkg/s3utils"
    13  	"github.com/minio/minio-go/v7/pkg/signer"
    14  	"github.com/treeverse/lakefs/pkg/auth/model"
    15  	gwErrors "github.com/treeverse/lakefs/pkg/gateway/errors"
    16  	"github.com/treeverse/lakefs/pkg/gateway/sig"
    17  	"github.com/treeverse/lakefs/pkg/testutil"
    18  )
    19  
    20  func makeRequest(t *testing.T, headers map[string]string, query map[string]string) *http.Request {
    21  	r, err := http.NewRequest("GET", "https://example.com", nil)
    22  	if err != nil {
    23  		t.Fatal(err)
    24  	}
    25  	for k, v := range headers {
    26  		r.Header.Set(k, v)
    27  	}
    28  	q := r.URL.Query()
    29  	for k, v := range query {
    30  		q.Add(k, v)
    31  	}
    32  	r.URL.RawQuery = q.Encode()
    33  	return r
    34  }
    35  
    36  func TestIsAWSSignedRequest(t *testing.T) {
    37  	t.Parallel()
    38  	cases := []struct {
    39  		Name    string
    40  		Want    bool
    41  		Headers map[string]string
    42  		Query   map[string]string
    43  	}{
    44  		{Name: "no sig", Want: false},
    45  		{Name: "non aws auth header", Want: false, Headers: map[string]string{"Authorization": "Basic dXNlcjpwYXNzd29yZA=="}},
    46  		{Name: "v2 auth header", Want: true, Headers: map[string]string{"Authorization": "AWS wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}},
    47  		{Name: "v2 auth query param", Want: true, Query: map[string]string{"AWSAccessKeyId": "bPxRfiCYEXAMPLEKEY"}},
    48  		{Name: "v4 auth header", Want: true, Headers: map[string]string{"Authorization": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7"}},
    49  		{Name: "v4 auth query param", Want: true, Query: map[string]string{"X-Amz-Credential": "bPxRfiCYEXAMPLEKEY"}},
    50  	}
    51  
    52  	for _, tc := range cases {
    53  		t.Run(tc.Name, func(t *testing.T) {
    54  			r := makeRequest(t, tc.Headers, tc.Query)
    55  			got := sig.IsAWSSignedRequest(r)
    56  			if got != tc.Want {
    57  				t.Fatalf("IsAWSSignedRequest with %s: got %v, expected %v", tc.Name, got, tc.Want)
    58  			}
    59  		})
    60  	}
    61  }
    62  
    63  type (
    64  	Signer   func(req http.Request) *http.Request
    65  	Verifier func(req *http.Request) error
    66  )
    67  
    68  type Style string
    69  
    70  func MakeV2Signer(keyID, secretKey string, style Style) Signer {
    71  	return func(req http.Request) *http.Request {
    72  		return signer.SignV2(req, keyID, secretKey, style == "host")
    73  	}
    74  }
    75  
    76  func MakeV4Signer(keyID, secretKey, location string) Signer {
    77  	return func(req http.Request) *http.Request {
    78  		return signer.SignV4(req, keyID, secretKey, "", location)
    79  	}
    80  }
    81  
    82  func MakeV2Verifier(keyID, secretKey, bareDomain string) Verifier {
    83  	return func(req *http.Request) error {
    84  		authenticator := sig.NewV2SigAuthenticator(req, bareDomain)
    85  		_, err := authenticator.Parse()
    86  		if err != nil {
    87  			return fmt.Errorf("sigV2 parse failed: %w", err)
    88  		}
    89  		return authenticator.Verify(
    90  			&model.Credential{
    91  				BaseCredential: model.BaseCredential{
    92  					AccessKeyID:     keyID,
    93  					SecretAccessKey: secretKey,
    94  				},
    95  			},
    96  		)
    97  	}
    98  }
    99  
   100  func MakeV4Verifier(keyID, secretKey string) Verifier {
   101  	return func(req *http.Request) error {
   102  		authenticator := sig.NewV4Authenticator(req)
   103  		_, err := authenticator.Parse()
   104  		if err != nil {
   105  			return fmt.Errorf("sigV4 parse failed: %w", err)
   106  		}
   107  		return authenticator.Verify(
   108  			&model.Credential{BaseCredential: model.BaseCredential{AccessKeyID: keyID, SecretAccessKey: secretKey}},
   109  		)
   110  	}
   111  }
   112  
   113  func MakeHeader(m map[string]string) http.Header {
   114  	ret := http.Header{}
   115  	for k, v := range m {
   116  		ret.Add(k, v)
   117  	}
   118  	return ret
   119  }
   120  
   121  const (
   122  	keyID     = "AKIAIOSFODNN7EXAMPLE"
   123  	secretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
   124  	domain    = "s3.lakefs.test"
   125  	location  = "lu-alpha-1"
   126  )
   127  
   128  var date = time.Unix(1631523198, 0)
   129  
   130  type SignCase struct {
   131  	Name     string
   132  	Signer   Signer
   133  	Verifier Verifier
   134  	Style    Style
   135  }
   136  
   137  var signatures = []SignCase{
   138  	{
   139  		Name:     "V2Host",
   140  		Signer:   MakeV2Signer(keyID, secretKey, "host"),
   141  		Verifier: MakeV2Verifier(keyID, secretKey, domain),
   142  		Style:    "host",
   143  	}, {
   144  		Name:     "V2Path",
   145  		Signer:   MakeV2Signer(keyID, secretKey, "path"),
   146  		Verifier: MakeV2Verifier(keyID, secretKey, domain),
   147  		Style:    "path",
   148  	}, {
   149  		Name:     "V4Host",
   150  		Signer:   MakeV4Signer(keyID, secretKey, location),
   151  		Verifier: MakeV4Verifier(keyID, secretKey),
   152  		Style:    "host",
   153  	}, {
   154  		Name:     "V4Path",
   155  		Signer:   MakeV4Signer(keyID, secretKey, location),
   156  		Verifier: MakeV4Verifier(keyID, secretKey),
   157  		Style:    "path",
   158  	},
   159  }
   160  
   161  func TestAWSSigVerify(t *testing.T) {
   162  	t.Parallel()
   163  	const (
   164  		numRounds  = 100
   165  		seed       = 20210913
   166  		pathLength = 900
   167  		bucket     = "my-bucket"
   168  	)
   169  	methods := []string{"GET", "PUT", "DELETE", "PATCH"}
   170  
   171  	for _, s := range signatures {
   172  		t.Run("Sig"+s.Name, func(t *testing.T) {
   173  			host := domain
   174  			if s.Style == "host" {
   175  				host = fmt.Sprintf("%s.%s", bucket, domain)
   176  			}
   177  
   178  			r := rand.New(rand.NewSource(seed))
   179  			for i := 0; i < numRounds; i++ {
   180  				path := s3utils.EncodePath("my-branch/ariels/x/" + testutil.RandomString(r, pathLength))
   181  				bucketURL := &url.URL{
   182  					Scheme: "s3",
   183  					Host:   bucket,
   184  					Path:   path,
   185  					// No way to construct possibly-equivalent forms, so let
   186  					// URL construct The Right RawPath.
   187  					RawPath: "",
   188  				}
   189  				req := http.Request{
   190  					Method: methods[r.Intn(len(methods))],
   191  					Host:   host,
   192  					URL:    bucketURL,
   193  					Header: MakeHeader(map[string]string{
   194  						"Date":         date.Format(http.TimeFormat),
   195  						"x-amz-date":   date.Format("20060102T150405Z"),
   196  						"Content-Md5":  "deadbeef",
   197  						"Content-Type": "application/binary",
   198  					}),
   199  				}
   200  				signedReq := s.Signer(req)
   201  				err := s.Verifier(signedReq)
   202  				if err != nil {
   203  					errText := err.Error()
   204  					var apiErr gwErrors.APIErrorCode
   205  					if errors.As(err, &apiErr) {
   206  						errText = apiErr.ToAPIErr().Description
   207  					}
   208  					t.Errorf("Sign and verify %s: %s", bucketURL.String(), errText)
   209  				}
   210  			}
   211  		})
   212  	}
   213  }