From 3573a31ae6c3d6d943b5688294866f30d106c96c Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Fri, 25 Aug 2023 21:50:21 +0400 Subject: [PATCH 1/3] fix: Closes #192, Fixed authentication errors returned, created integration test cases for it --- integration/action-tests.go | 7 +++ integration/tests.go | 89 ++++++++++++++++++++++++++++ integration/utils.go | 91 +++++++++++++++++++++++++---- s3api/controllers/base_test.go | 8 +-- s3api/middlewares/authentication.go | 54 ++++++++++++++++- s3err/s3err.go | 25 +++++--- 6 files changed, 251 insertions(+), 23 deletions(-) diff --git a/integration/action-tests.go b/integration/action-tests.go index 3c010dba..7e4fce80 100644 --- a/integration/action-tests.go +++ b/integration/action-tests.go @@ -1,5 +1,11 @@ package integration +func TestAuthentication(s *S3Conf) { + Authentication_empty_auth_header(s) + Authentication_invalid_auth_header(s) + Authentication_unsupported_signature_version(s) +} + func TestCreateBucket(s *S3Conf) { CreateBucket_invalid_bucket_name(s) CreateBucket_existing_bucket(s) @@ -147,6 +153,7 @@ func TestGetBucketAcl(s *S3Conf) { } func TestFullFlow(s *S3Conf) { + TestAuthentication(s) TestCreateBucket(s) TestHeadBucket(s) TestDeleteBucket(s) diff --git a/integration/tests.go b/integration/tests.go index 39819ce8..99d765d8 100644 --- a/integration/tests.go +++ b/integration/tests.go @@ -7,6 +7,8 @@ import ( "fmt" "io" "math" + "net/http" + "strings" "sync" "time" @@ -20,6 +22,93 @@ var ( shortTimeout = 10 * time.Second ) +func Authentication_empty_auth_header(s *S3Conf) { + testName := "Authentication_empty_auth_header" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + req.Header.Set("Authorization", "") + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrAuthHeaderEmpty)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_invalid_auth_header(s *S3Conf) { + testName := "Authentication_invalid_auth_header" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + req.Header.Set("Authorization", "invalid header") + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMissingFields)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_unsupported_signature_version(s *S3Conf) { + testName := "Authentication_unsupported_signature_version" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + authHdr := req.Header.Get("Authorization") + authHdr = strings.Replace(authHdr, "AWS4-HMAC-SHA256", "AWS2-HMAC-SHA1", 1) + req.Header.Set("Authorization", authHdr) + + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported)); err != nil { + return err + } + + return nil + }) +} + func CreateBucket_invalid_bucket_name(s *S3Conf) { testName := "CreateBucket_invalid_bucket_name" runF(testName) diff --git a/integration/utils.go b/integration/utils.go index 42babc1f..ac71d110 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -5,13 +5,19 @@ import ( "context" "crypto/rand" "crypto/sha256" + "encoding/hex" + "encoding/xml" "errors" "fmt" "io" + "net/http" "os" "os/exec" "strings" + "time" + "github.com/aws/aws-sdk-go-v2/aws" + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go" @@ -115,18 +121,66 @@ func actionHandler(s *S3Conf, testName string, handler func(s3client *s3.Client, } } -func putObjects(client *s3.Client, objs []string, bucket string) error { - for _, key := range objs { - ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) - _, err := client.PutObject(ctx, &s3.PutObjectInput{ - Key: &key, - Bucket: &bucket, - }) - cancel() - if err != nil { - return err - } +type authConfig struct { + testName string + path string + method string + body []byte + service string + date time.Time +} + +func authHandler(s *S3Conf, cfg *authConfig, handler func(req *http.Request) error) { + runF(cfg.testName) + req, err := http.NewRequest(cfg.method, fmt.Sprintf("%v/%v", s.endpoint, cfg.path), bytes.NewReader(cfg.body)) + if err != nil { + failF("%v: failed to send the request: %v", cfg.testName, err.Error()) + return } + + signer := v4.NewSigner() + + hashedPayload := sha256.Sum256([]byte{}) + hexPayload := hex.EncodeToString(hashedPayload[:]) + + req.Header.Set("X-Amz-Content-Sha256", hexPayload) + + signErr := signer.SignHTTP(req.Context(), aws.Credentials{AccessKeyID: s.awsID, SecretAccessKey: s.awsSecret}, req, hexPayload, cfg.service, s.awsRegion, cfg.date) + if signErr != nil { + failF("%v: failed to sign the request: %v", cfg.testName, err.Error()) + return + } + + err = handler(req) + if err != nil { + failF("%v: %v", cfg.testName, err.Error()) + return + } + passF(cfg.testName) +} + +func checkAuthErr(resp *http.Response, apiErr s3err.APIError) error { + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + var errResp s3err.APIErrorResponse + err = xml.Unmarshal(body, &errResp) + if err != nil { + return err + } + + if resp.StatusCode != apiErr.HTTPStatusCode { + return fmt.Errorf("expected response status code to be %v, instead got %v", apiErr.HTTPStatusCode, resp.StatusCode) + } + if errResp.Code != apiErr.Code { + return fmt.Errorf("expected error code to be %v, instead got %v", apiErr.Code, errResp.Code) + } + if errResp.Message != apiErr.Description { + return fmt.Errorf("expected error message to be %v, instead got %v", apiErr.Description, errResp.Message) + } + return nil } @@ -157,6 +211,21 @@ func checkSdkApiErr(err error, code string) error { return err } +func putObjects(client *s3.Client, objs []string, bucket string) error { + for _, key := range objs { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := client.PutObject(ctx, &s3.PutObjectInput{ + Key: &key, + Bucket: &bucket, + }) + cancel() + if err != nil { + return err + } + } + return nil +} + func putObjectWithData(lgth int64, input *s3.PutObjectInput, client *s3.Client) (csum [32]byte, data []byte, err error) { data = make([]byte, lgth) rand.Read(data) diff --git a/s3api/controllers/base_test.go b/s3api/controllers/base_test.go index c9403eb6..97115bb9 100644 --- a/s3api/controllers/base_test.go +++ b/s3api/controllers/base_test.go @@ -1447,7 +1447,7 @@ func Test_XMLresponse(t *testing.T) { args: args{ ctx: &ctx, resp: nil, - err: s3err.GetAPIError(16), + err: s3err.GetAPIError(s3err.ErrInternalError), }, wantErr: false, statusCode: 500, @@ -1457,7 +1457,7 @@ func Test_XMLresponse(t *testing.T) { args: args{ ctx: &ctx, resp: nil, - err: s3err.GetAPIError(50), + err: s3err.GetAPIError(s3err.ErrNotImplemented), }, wantErr: false, statusCode: 501, @@ -1527,7 +1527,7 @@ func Test_response(t *testing.T) { args: args{ ctx: &ctx, resp: nil, - err: s3err.GetAPIError(16), + err: s3err.GetAPIError(s3err.ErrInternalError), }, wantErr: false, statusCode: 500, @@ -1547,7 +1547,7 @@ func Test_response(t *testing.T) { args: args{ ctx: &ctx, resp: nil, - err: s3err.GetAPIError(50), + err: s3err.GetAPIError(s3err.ErrNotImplemented), }, wantErr: false, statusCode: 501, diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index b46aeb31..d0e34881 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -17,6 +17,8 @@ package middlewares import ( "crypto/sha256" "encoding/hex" + "fmt" + "net/http" "os" "strings" "time" @@ -34,6 +36,7 @@ import ( const ( iso8601Format = "20060102T150405Z" + YYYYMMDD = "20060102" ) type RootUserConfig struct { @@ -72,10 +75,30 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au if len(credKv) != 2 { return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed), &controllers.MetaOpts{Logger: logger}) } + // Credential variables validation creds := strings.Split(credKv[1], "/") - if len(creds) < 4 { + if len(creds) < 5 { return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed), &controllers.MetaOpts{Logger: logger}) } + if creds[4] != "aws4_request" { + return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureTerminationStr), &controllers.MetaOpts{Logger: logger}) + } + if creds[3] != "s3" { + return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureIncorrService), &controllers.MetaOpts{Logger: logger}) + } + if creds[2] != region { + return controllers.SendResponse(ctx, s3err.APIError{ + Code: "SignatureDoesNotMatch", + Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", creds[2]), + HTTPStatusCode: http.StatusForbidden, + }, &controllers.MetaOpts{Logger: logger}) + } + + // Validate the dates difference + err := validateDate(creds[1]) + if err != nil { + return controllers.SendResponse(ctx, err, &controllers.MetaOpts{Logger: logger}) + } ctx.Locals("access", creds[0]) ctx.Locals("isRoot", creds[0] == root.Access) @@ -101,6 +124,10 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingDateHeader), &controllers.MetaOpts{Logger: logger}) } + if date[:8] != creds[1] { + return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch), &controllers.MetaOpts{Logger: logger}) + } + // Parse the date and check the date validity tdate, err := time.Parse(iso8601Format, date) if err != nil { @@ -186,3 +213,28 @@ func isSpecialPayload(str string) bool { return specialValues[str] } + +func validateDate(date string) error { + credDate, err := time.Parse(YYYYMMDD, date) + if err != nil { + return s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch) + } + + today := time.Now() + if credDate.Year() > today.Year() || (credDate.Year() == today.Year() && credDate.YearDay() > today.YearDay()) { + return s3err.APIError{ + Code: "SignatureDoesNotMatch", + Description: fmt.Sprintf("Signature not yet current: %s is still later than %s", credDate.Format(YYYYMMDD), today.Format(YYYYMMDD)), + HTTPStatusCode: http.StatusForbidden, + } + } + if credDate.Year() < today.Year() || (credDate.Year() == today.Year() && credDate.YearDay() < today.YearDay()) { + return s3err.APIError{ + Code: "SignatureDoesNotMatch", + Description: fmt.Sprintf("Signature expired: %s is now earlier than %s", credDate.Format(YYYYMMDD), today.Format(YYYYMMDD)), + HTTPStatusCode: http.StatusForbidden, + } + } + + return nil +} diff --git a/s3err/s3err.go b/s3err/s3err.go index b67cedff..82e791fd 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -97,6 +97,9 @@ const ( ErrNegativeExpires ErrMaximumExpires ErrSignatureDoesNotMatch + ErrSignatureDateDoesNotMatch + ErrSignatureTerminationStr + ErrSignatureIncorrService ErrContentSHA256Mismatch ErrInvalidAccessKeyID ErrRequestNotReadyYet @@ -190,13 +193,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "We encountered an internal error, please try again.", HTTPStatusCode: http.StatusInternalServerError, }, - ErrInvalidPart: { Code: "InvalidPart", Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidCopyDest: { Code: "InvalidRequest", Description: "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.", @@ -287,7 +288,6 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "Signature header missing Signature field.", HTTPStatusCode: http.StatusBadRequest, }, - ErrUnsignedHeaders: { Code: "AccessDenied", Description: "There were headers present in the request which were not signed", @@ -323,25 +323,36 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds", HTTPStatusCode: http.StatusBadRequest, }, - ErrInvalidAccessKeyID: { Code: "InvalidAccessKeyId", Description: "The access key ID you provided does not exist in our records.", HTTPStatusCode: http.StatusForbidden, }, - ErrRequestNotReadyYet: { Code: "AccessDenied", Description: "Request is not valid yet", HTTPStatusCode: http.StatusForbidden, }, - ErrSignatureDoesNotMatch: { Code: "SignatureDoesNotMatch", Description: "The request signature we calculated does not match the signature you provided. Check your key and signing method.", HTTPStatusCode: http.StatusForbidden, }, - + ErrSignatureDateDoesNotMatch: { + Code: "SignatureDoesNotMatch", + Description: "Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP", + HTTPStatusCode: http.StatusForbidden, + }, + ErrSignatureTerminationStr: { + Code: "SignatureDoesNotMatch", + Description: "Credential should be scoped with a valid terminator: 'aws4_request'", + HTTPStatusCode: http.StatusForbidden, + }, + ErrSignatureIncorrService: { + Code: "SignatureDoesNotMatch", + Description: "Credential should be scoped to correct service: s3", + HTTPStatusCode: http.StatusForbidden, + }, ErrContentSHA256Mismatch: { Code: "XAmzContentSHA256Mismatch", Description: "The provided 'x-amz-content-sha256' header does not match what was computed.", From a58ce0c238b2b9b7706c3d246fd4fe018c9638a6 Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Tue, 29 Aug 2023 23:46:54 +0400 Subject: [PATCH 2/3] feat: Added 8 integration test cases for authentication --- integration/action-tests.go | 8 + integration/tests.go | 283 ++++++++++++++++++++++++++++ s3api/middlewares/authentication.go | 2 +- 3 files changed, 292 insertions(+), 1 deletion(-) diff --git a/integration/action-tests.go b/integration/action-tests.go index 7e4fce80..67210769 100644 --- a/integration/action-tests.go +++ b/integration/action-tests.go @@ -4,6 +4,14 @@ func TestAuthentication(s *S3Conf) { Authentication_empty_auth_header(s) Authentication_invalid_auth_header(s) Authentication_unsupported_signature_version(s) + Authentication_malformed_credentials(s) + Authentication_malformed_credentials_invalid_parts(s) + Authentication_credentials_terminated_string(s) + Authentication_credentials_incorrect_service(s) + Authentication_credentials_incorrect_region(s) + Authentication_credentials_invalid_date(s) + Authentication_credentials_future_date(s) + Authentication_credentials_past_date(s) } func TestCreateBucket(s *S3Conf) { diff --git a/integration/tests.go b/integration/tests.go index 99d765d8..ce9c5f18 100644 --- a/integration/tests.go +++ b/integration/tests.go @@ -3,11 +3,13 @@ package integration import ( "context" "crypto/sha256" + "encoding/xml" "errors" "fmt" "io" "math" "net/http" + "regexp" "strings" "sync" "time" @@ -109,6 +111,287 @@ func Authentication_unsupported_signature_version(s *S3Conf) { }) } +func Authentication_malformed_credentials(s *S3Conf) { + testName := "Authentication_malformed_credentials" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + authHdr := req.Header.Get("Authorization") + regExp := regexp.MustCompile("Credential=[^,]+,") + hdr := regExp.ReplaceAllString(authHdr, "Credential-access/32234/us-east-1/s3/aws4_request,") + req.Header.Set("Authorization", hdr) + + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_malformed_credentials_invalid_parts(s *S3Conf) { + testName := "Authentication_malformed_credentials_invalid_parts" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + authHdr := req.Header.Get("Authorization") + regExp := regexp.MustCompile("Credential=[^,]+,") + hdr := regExp.ReplaceAllString(authHdr, "Credential=access/32234/us-east-1/s3,") + req.Header.Set("Authorization", hdr) + + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_credentials_terminated_string(s *S3Conf) { + testName := "Authentication_credentials_terminated_string" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + authHdr := req.Header.Get("Authorization") + regExp := regexp.MustCompile("Credential=[^,]+,") + hdr := regExp.ReplaceAllString(authHdr, "Credential=access/32234/us-east-1/s3/aws_request,") + req.Header.Set("Authorization", hdr) + + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_credentials_incorrect_service(s *S3Conf) { + testName := "Authentication_credentials_incorrect_service" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "ec2", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureIncorrService)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_credentials_incorrect_region(s *S3Conf) { + testName := "Authentication_credentials_incorrect_region" + cfg := *s + if cfg.awsRegion == "us-east-1" { + cfg.awsRegion = "us-west-1" + } else { + cfg.awsRegion = "us-east-1" + } + authHandler(&cfg, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + apiErr := s3err.APIError{ + Code: "SignatureDoesNotMatch", + Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", cfg.awsRegion), + HTTPStatusCode: http.StatusForbidden, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, apiErr); err != nil { + return err + } + + return nil + }) +} + +func Authentication_credentials_invalid_date(s *S3Conf) { + testName := "Authentication_credentials_invalid_date" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + authHdr := req.Header.Get("Authorization") + regExp := regexp.MustCompile("Credential=[^,]+,") + hdr := regExp.ReplaceAllString(authHdr, "Credential=access/3223423234/us-east-1/s3/aws4_request,") + req.Header.Set("Authorization", hdr) + + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_credentials_future_date(s *S3Conf) { + testName := "Authentication_credentials_future_date" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now().Add(time.Duration(5) * 24 * time.Hour), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + var errResp s3err.APIErrorResponse + err = xml.Unmarshal(body, &errResp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusForbidden { + return fmt.Errorf("expected response status code to be %v, instead got %v", http.StatusForbidden, resp.StatusCode) + } + if errResp.Code != "SignatureDoesNotMatch" { + return fmt.Errorf("expected error code to be %v, instead got %v", "SignatureDoesNotMatch", errResp.Code) + } + if !strings.Contains(errResp.Message, "Signature not yet current:") { + return fmt.Errorf("expected future date error message, instead got %v", errResp.Message) + } + + return nil + }) +} + +func Authentication_credentials_past_date(s *S3Conf) { + testName := "Authentication_credentials_past_date" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now().Add(time.Duration(-5) * 24 * time.Hour), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + var errResp s3err.APIErrorResponse + err = xml.Unmarshal(body, &errResp) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusForbidden { + return fmt.Errorf("expected response status code to be %v, instead got %v", http.StatusForbidden, resp.StatusCode) + } + if errResp.Code != "SignatureDoesNotMatch" { + return fmt.Errorf("expected error code to be %v, instead got %v", "SignatureDoesNotMatch", errResp.Code) + } + if !strings.Contains(errResp.Message, "Signature expired:") { + return fmt.Errorf("expected past date error message, instead got %v", errResp.Message) + } + + return nil + }) +} + func CreateBucket_invalid_bucket_name(s *S3Conf) { testName := "CreateBucket_invalid_bucket_name" runF(testName) diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index d0e34881..dc0efe1d 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -77,7 +77,7 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au } // Credential variables validation creds := strings.Split(credKv[1], "/") - if len(creds) < 5 { + if len(creds) != 5 { return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrCredMalformed), &controllers.MetaOpts{Logger: logger}) } if creds[4] != "aws4_request" { From 53cf4f342fa438c493a5e67e59e1ef10c4e56d3b Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Wed, 30 Aug 2023 23:21:09 +0400 Subject: [PATCH 3/3] feat: Added more integration test cases for the authentication and md5 checker --- integration/action-tests.go | 8 + integration/tests.go | 234 ++++++++++++++++++++++++++++ s3api/middlewares/authentication.go | 8 +- 3 files changed, 246 insertions(+), 4 deletions(-) diff --git a/integration/action-tests.go b/integration/action-tests.go index 67210769..c3ea1b5c 100644 --- a/integration/action-tests.go +++ b/integration/action-tests.go @@ -12,6 +12,14 @@ func TestAuthentication(s *S3Conf) { Authentication_credentials_invalid_date(s) Authentication_credentials_future_date(s) Authentication_credentials_past_date(s) + Authentication_credentials_non_existing_access_key(s) + Authentication_invalid_signed_headers(s) + Authentication_missing_date_header(s) + Authentication_invalid_date_header(s) + Authentication_date_mismatch(s) + Authentication_incorrect_payload_hash(s) + Authentication_incorrect_md5(s) + Authentication_signature_error_incorrect_secret_key(s) } func TestCreateBucket(s *S3Conf) { diff --git a/integration/tests.go b/integration/tests.go index ce9c5f18..30d12bd3 100644 --- a/integration/tests.go +++ b/integration/tests.go @@ -392,6 +392,240 @@ func Authentication_credentials_past_date(s *S3Conf) { }) } +func Authentication_credentials_non_existing_access_key(s *S3Conf) { + testName := "Authentication_credentials_non_existing_access_key" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + authHdr := req.Header.Get("Authorization") + regExp := regexp.MustCompile("Credential=([^/]+)") + hdr := regExp.ReplaceAllString(authHdr, "Credential=a_rarely_existing_access_key_id_a7s86df78as6df89790a8sd7f") + req.Header.Set("Authorization", hdr) + + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_invalid_signed_headers(s *S3Conf) { + testName := "Authentication_invalid_signed_headers" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + authHdr := req.Header.Get("Authorization") + regExp := regexp.MustCompile("SignedHeaders=[^,]+,") + hdr := regExp.ReplaceAllString(authHdr, "SignedHeaders-host;x-amz-content-sha256;x-amz-date,") + req.Header.Set("Authorization", hdr) + + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_missing_date_header(s *S3Conf) { + testName := "Authentication_missing_date_header" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + req.Header.Set("X-Amz-Date", "") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMissingDateHeader)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_invalid_date_header(s *S3Conf) { + testName := "Authentication_invalid_date_header" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + req.Header.Set("X-Amz-Date", "03032006") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMalformedDate)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_date_mismatch(s *S3Conf) { + testName := "Authentication_date_mismatch" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + req.Header.Set("X-Amz-Date", "20220830T095525Z") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_incorrect_payload_hash(s *S3Conf) { + testName := "Authentication_incorrect_payload_hash" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + req.Header.Set("X-Amz-Content-Sha256", "7sa6df576dsa5f675sad67f") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrContentSHA256Mismatch)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_incorrect_md5(s *S3Conf) { + testName := "Authentication_incorrect_md5" + authHandler(s, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + + req.Header.Set("Content-Md5", "sadfasdf87sad6f87==") + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidDigest)); err != nil { + return err + } + + return nil + }) +} + +func Authentication_signature_error_incorrect_secret_key(s *S3Conf) { + testName := "Authentication_signature_error_incorrect_secret_key" + cfg := *s + cfg.awsSecret = s.awsSecret + "a" + authHandler(&cfg, &authConfig{ + testName: testName, + path: "my-bucket", + method: http.MethodGet, + body: nil, + service: "s3", + date: time.Now(), + }, func(req *http.Request) error { + client := http.Client{ + Timeout: shortTimeout, + } + + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch)); err != nil { + return err + } + + return nil + }) +} + func CreateBucket_invalid_bucket_name(s *S3Conf) { testName := "CreateBucket_invalid_bucket_name" runF(testName) diff --git a/s3api/middlewares/authentication.go b/s3api/middlewares/authentication.go index dc0efe1d..001369bc 100644 --- a/s3api/middlewares/authentication.go +++ b/s3api/middlewares/authentication.go @@ -124,16 +124,16 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMissingDateHeader), &controllers.MetaOpts{Logger: logger}) } - if date[:8] != creds[1] { - return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch), &controllers.MetaOpts{Logger: logger}) - } - // Parse the date and check the date validity tdate, err := time.Parse(iso8601Format, date) if err != nil { return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrMalformedDate), &controllers.MetaOpts{Logger: logger}) } + if date[:8] != creds[1] { + return controllers.SendResponse(ctx, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch), &controllers.MetaOpts{Logger: logger}) + } + hashPayloadHeader := ctx.Get("X-Amz-Content-Sha256") ok := isSpecialPayload(hashPayloadHeader)