From 4ca56d3d6e8172fb14278a2843a617a489f8e064 Mon Sep 17 00:00:00 2001 From: Denis Kirillov Date: Mon, 9 Aug 2021 12:35:42 +0300 Subject: [PATCH] [#199] Add fine-grained handle auth header Signed-off-by: Denis Kirillov --- api/auth/center.go | 96 ++++++++++++++++++++++++++--------- api/auth/center_test.go | 87 +++++++++++++++++++++++++++++++ api/auth/regexp-utils_test.go | 16 ------ api/response.go | 3 +- 4 files changed, 161 insertions(+), 41 deletions(-) create mode 100644 api/auth/center_test.go delete mode 100644 api/auth/regexp-utils_test.go diff --git a/api/auth/center.go b/api/auth/center.go index 8ffbf383..4b82ad05 100644 --- a/api/auth/center.go +++ b/api/auth/center.go @@ -22,7 +22,7 @@ import ( ) // authorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter. -var authorizationFieldRegexp = regexp.MustCompile(`AWS4-HMAC-SHA256 Credential=(?P[^/]+)0(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request,\s*SignedHeaders=(?P.+),\s*Signature=(?P.+)`) +var authorizationFieldRegexp = regexp.MustCompile(`AWS4-HMAC-SHA256 Credential=(?P[^/]+)/(?P[^/]+)/(?P[^/]*)/(?P[^/]+)/aws4_request,\s*SignedHeaders=(?P.+),\s*Signature=(?P.+)`) type ( // Center is a user authentication interface. @@ -42,6 +42,20 @@ type ( } prs int + + authHeader struct { + AccessKeyID string + Service string + Region string + SignatureV4 string + SignedFields []string + Date string + } +) + +const ( + accessKeyPartsNum = 2 + authHeaderPartsNum = 6 ) // ErrNoAuthorizationHeader is returned for unauthenticated requests. @@ -65,6 +79,37 @@ func New(conns pool.Pool, key *keys.PrivateKey) Center { } } +func (c *center) parseAuthHeader(header string) (*authHeader, error) { + submatches := c.reg.getSubmatches(header) + if len(submatches) != authHeaderPartsNum { + return nil, apiErrors.GetAPIError(apiErrors.ErrAuthorizationHeaderMalformed) + } + + accessKey := strings.Split(submatches["access_key_id"], "0") + if len(accessKey) != accessKeyPartsNum { + return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidAccessKeyID) + } + + signedFields := strings.Split(submatches["signed_header_fields"], ";") + + return &authHeader{ + AccessKeyID: submatches["access_key_id"], + Service: submatches["service"], + Region: submatches["region"], + SignatureV4: submatches["v4_signature"], + SignedFields: signedFields, + Date: submatches["date"], + }, nil +} + +func (a *authHeader) getAddress() (*object.Address, error) { + address := object.NewAddress() + if err := address.Parse(strings.ReplaceAll(a.AccessKeyID, "0", "/")); err != nil { + return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidAccessKeyID) + } + return address, nil +} + func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) { queryValues := r.URL.Query() if queryValues.Get("X-Amz-Algorithm") == "AWS4-HMAC-SHA256" { @@ -76,14 +121,9 @@ func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) { return nil, ErrNoAuthorizationHeader } - sms1 := c.reg.getSubmatches(authHeaderField[0]) - if len(sms1) != 7 { - return nil, apiErrors.GetAPIError(apiErrors.ErrAuthorizationHeaderMalformed) - } - - signedHeaderFieldsNames := strings.Split(sms1["signed_header_fields"], ";") - if len(signedHeaderFieldsNames) == 0 { - return nil, errors.New("wrong format of signed headers part") + authHeader, err := c.parseAuthHeader(authHeaderField[0]) + if err != nil { + return nil, err } signatureDateTime, err := time.Parse("20060102T150405Z", r.Header.Get("X-Amz-Date")) @@ -91,12 +131,9 @@ func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) { return nil, fmt.Errorf("failed to parse x-amz-date header field: %w", err) } - accessKeyID := fmt.Sprintf("%s0%s", sms1["access_key_id_cid"], sms1["access_key_id_oid"]) - accessKeyAddress := fmt.Sprintf("%s/%s", sms1["access_key_id_cid"], sms1["access_key_id_oid"]) - - address := object.NewAddress() - if err = address.Parse(accessKeyAddress); err != nil { - return nil, fmt.Errorf("could not parse AccessBox address: %s : %w", accessKeyID, err) + address, err := authHeader.getAddress() + if err != nil { + return nil, err } box, err := c.cli.GetBox(r.Context(), address) @@ -104,30 +141,43 @@ func (c *center) Authenticate(r *http.Request) (*accessbox.Box, error) { return nil, err } + clonedRequest := cloneRequest(r, authHeader) + if err = c.checkSign(authHeader, box, clonedRequest, signatureDateTime); err != nil { + return nil, err + } + + return box, nil +} + +func cloneRequest(r *http.Request, authHeader *authHeader) *http.Request { otherRequest := r.Clone(context.TODO()) otherRequest.Header = make(http.Header) for key, val := range r.Header { - for _, name := range signedHeaderFieldsNames { + for _, name := range authHeader.SignedFields { if strings.EqualFold(key, name) { otherRequest.Header[key] = val } } } - awsCreds := credentials.NewStaticCredentials(accessKeyID, box.Gate.AccessKey, "") + return otherRequest +} + +func (c *center) checkSign(authHeader *authHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error { + awsCreds := credentials.NewStaticCredentials(authHeader.AccessKeyID, box.Gate.AccessKey, "") signer := v4.NewSigner(awsCreds) signer.DisableURIPathEscaping = true // body not required - if _, err := signer.Sign(otherRequest, nil, sms1["service"], sms1["region"], signatureDateTime); err != nil { - return nil, fmt.Errorf("failed to sign temporary HTTP request: %w", err) + if _, err := signer.Sign(request, nil, authHeader.Service, authHeader.Region, signatureDateTime); err != nil { + return fmt.Errorf("failed to sign temporary HTTP request: %w", err) } - sms2 := c.reg.getSubmatches(otherRequest.Header.Get("Authorization")) - if sms1["v4_signature"] != sms2["v4_signature"] { - return nil, apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch) + sms2 := c.reg.getSubmatches(request.Header.Get("Authorization")) + if authHeader.SignatureV4 != sms2["v4_signature"] { + return apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch) } - return box, nil + return nil } diff --git a/api/auth/center_test.go b/api/auth/center_test.go new file mode 100644 index 00000000..3e7481d2 --- /dev/null +++ b/api/auth/center_test.go @@ -0,0 +1,87 @@ +package auth + +import ( + "strings" + "testing" + + "github.com/nspcc-dev/neofs-s3-gw/api/errors" + "github.com/stretchr/testify/require" +) + +func TestAuthHeaderParse(t *testing.T) { + defaultHeader := "AWS4-HMAC-SHA256 Credential=oid0cid/20210809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2811ccb9e242f41426738fb1f" + + center := ¢er{ + reg: ®expSubmatcher{re: authorizationFieldRegexp}, + } + + for _, tc := range []struct { + header string + err error + expected *authHeader + }{ + { + header: defaultHeader, + err: nil, + expected: &authHeader{ + AccessKeyID: "oid0cid", + Service: "s3", + Region: "us-east-1", + SignatureV4: "2811ccb9e242f41426738fb1f", + SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"}, + Date: "20210809", + }, + }, + { + header: strings.ReplaceAll(defaultHeader, "Signature=2811ccb9e242f41426738fb1f", ""), + err: errors.GetAPIError(errors.ErrAuthorizationHeaderMalformed), + expected: nil, + }, + { + header: strings.ReplaceAll(defaultHeader, "oid0cid", "oidcid"), + err: errors.GetAPIError(errors.ErrInvalidAccessKeyID), + expected: nil, + }, + } { + authHeader, err := center.parseAuthHeader(tc.header) + require.Equal(t, tc.err, err, tc.header) + require.Equal(t, tc.expected, authHeader, tc.header) + } +} + +func TestAuthHeaderGetAddress(t *testing.T) { + defaulErr := errors.GetAPIError(errors.ErrInvalidAccessKeyID) + + for _, tc := range []struct { + authHeader *authHeader + err error + }{ + { + authHeader: &authHeader{ + AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJM0HrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB", + }, + err: nil, + }, + { + authHeader: &authHeader{ + AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJMHrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB", + }, + err: defaulErr, + }, + { + authHeader: &authHeader{ + AccessKeyID: "oid0cid", + }, + err: defaulErr, + }, + { + authHeader: &authHeader{ + AccessKeyID: "oidcid", + }, + err: defaulErr, + }, + } { + _, err := tc.authHeader.getAddress() + require.Equal(t, tc.err, err, tc.authHeader.AccessKeyID) + } +} diff --git a/api/auth/regexp-utils_test.go b/api/auth/regexp-utils_test.go deleted file mode 100644 index fe853873..00000000 --- a/api/auth/regexp-utils_test.go +++ /dev/null @@ -1,16 +0,0 @@ -package auth - -import ( - "fmt" - "testing" -) - -func TestName(t *testing.T) { - //target:= "AWS4-HMAC-SHA256 Credential=vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJM0HrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB/20210809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2811ccb9e242f41426738fb1fa6a456ef37c63505da1a160f3d76a4f51b17581" - target := "AWS4-HMAC-SHA256 Credential=badauth/20210809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2811ccb9e242f41426738fb1fa6a456ef37c63505da1a160f3d76a4f51b17581" - - subMatcher := ®expSubmatcher{re: authorizationFieldRegexp} - - submatches := subMatcher.getSubmatches(target) - fmt.Println(submatches) -} diff --git a/api/response.go b/api/response.go index eaf2b8f0..b70b1c86 100644 --- a/api/response.go +++ b/api/response.go @@ -7,9 +7,8 @@ import ( "net/http" "strconv" - "github.com/nspcc-dev/neofs-s3-gw/api/errors" - "github.com/google/uuid" + "github.com/nspcc-dev/neofs-s3-gw/api/errors" "github.com/nspcc-dev/neofs-s3-gw/internal/version" )