diff --git a/integration/action-tests.go b/integration/action-tests.go index 3c010dba..c3ea1b5c 100644 --- a/integration/action-tests.go +++ b/integration/action-tests.go @@ -1,5 +1,27 @@ package integration +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) + 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) { CreateBucket_invalid_bucket_name(s) CreateBucket_existing_bucket(s) @@ -147,6 +169,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..30d12bd3 100644 --- a/integration/tests.go +++ b/integration/tests.go @@ -3,10 +3,14 @@ package integration import ( "context" "crypto/sha256" + "encoding/xml" "errors" "fmt" "io" "math" + "net/http" + "regexp" + "strings" "sync" "time" @@ -20,6 +24,608 @@ 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 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 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/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..001369bc 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) @@ -107,6 +130,10 @@ func VerifyV4Signature(root RootUserConfig, iam auth.IAMService, logger s3log.Au 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) @@ -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.",