From 6084fc3f3e849f0254a78309486569b1f87aebd6 Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Mon, 12 Feb 2024 16:08:31 -0500 Subject: [PATCH] feat: Added 20 integration tests for v4 authentication with query params. Fixed few bugs in v4 query params authentication --- integration/group-tests.go | 44 ++ integration/tests.go | 841 ++++++++++++++++++++++++++++- integration/utils.go | 38 ++ s3api/middlewares/presign-auth.go | 2 +- s3api/utils/presign-auth-reader.go | 87 ++- s3api/utils/utils.go | 17 +- 6 files changed, 1008 insertions(+), 21 deletions(-) diff --git a/integration/group-tests.go b/integration/group-tests.go index d0421c087..55ef11338 100644 --- a/integration/group-tests.go +++ b/integration/group-tests.go @@ -22,6 +22,29 @@ func TestAuthentication(s *S3Conf) { Authentication_signature_error_incorrect_secret_key(s) } +func TestPresignedAuthentication(s *S3Conf) { + PresignedAuth_missing_algo_query_param(s) + PresignedAuth_unsupported_algorithm(s) + PresignedAuth_missing_credentials_query_param(s) + PresignedAuth_malformed_creds_invalid_parts(s) + PresignedAuth_malformed_creds_invalid_parts(s) + PresignedAuth_creds_incorrect_service(s) + PresignedAuth_creds_incorrect_region(s) + PresignedAuth_creds_invalid_date(s) + PresignedAuth_missing_date_query(s) + PresignedAuth_dates_mismatch(s) + PresignedAuth_non_existing_access_key_id(s) + PresignedAuth_missing_signed_headers_query_param(s) + PresignedAuth_missing_expiration_query_param(s) + PresignedAuth_invalid_expiration_query_param(s) + PresignedAuth_negative_expiration_query_param(s) + PresignedAuth_exceeding_expiration_query_param(s) + PresignedAuth_expired_request(s) + PresignedAuth_incorrect_secret_key(s) + PresignedAuth_PutObject_success(s) + PresignedAuth_Put_GetObject_with_data(s) +} + func TestCreateBucket(s *S3Conf) { CreateBucket_invalid_bucket_name(s) CreateBucket_existing_bucket(s) @@ -219,6 +242,7 @@ func TestGetBucketAcl(s *S3Conf) { func TestFullFlow(s *S3Conf) { TestAuthentication(s) + TestPresignedAuthentication(s) TestCreateBucket(s) TestHeadBucket(s) TestListBuckets(s) @@ -277,6 +301,26 @@ func GetIntTests() IntTests { "Authentication_incorrect_payload_hash": Authentication_incorrect_payload_hash, "Authentication_incorrect_md5": Authentication_incorrect_md5, "Authentication_signature_error_incorrect_secret_key": Authentication_signature_error_incorrect_secret_key, + "PresignedAuth_missing_algo_query_param": PresignedAuth_missing_algo_query_param, + "PresignedAuth_unsupported_algorithm": PresignedAuth_unsupported_algorithm, + "PresignedAuth_missing_credentials_query_param": PresignedAuth_missing_credentials_query_param, + "PresignedAuth_malformed_creds_invalid_parts": PresignedAuth_malformed_creds_invalid_parts, + "PresignedAuth_creds_invalid_terminator": PresignedAuth_creds_invalid_terminator, + "PresignedAuth_creds_incorrect_service": PresignedAuth_creds_incorrect_service, + "PresignedAuth_creds_incorrect_region": PresignedAuth_creds_incorrect_region, + "PresignedAuth_creds_invalid_date": PresignedAuth_creds_invalid_date, + "PresignedAuth_missing_date_query": PresignedAuth_missing_date_query, + "PresignedAuth_dates_mismatch": PresignedAuth_dates_mismatch, + "PresignedAuth_non_existing_access_key_id": PresignedAuth_non_existing_access_key_id, + "PresignedAuth_missing_signed_headers_query_param": PresignedAuth_missing_signed_headers_query_param, + "PresignedAuth_missing_expiration_query_param": PresignedAuth_missing_expiration_query_param, + "PresignedAuth_invalid_expiration_query_param": PresignedAuth_invalid_expiration_query_param, + "PresignedAuth_negative_expiration_query_param": PresignedAuth_negative_expiration_query_param, + "PresignedAuth_exceeding_expiration_query_param": PresignedAuth_exceeding_expiration_query_param, + "PresignedAuth_expired_request": PresignedAuth_expired_request, + "PresignedAuth_incorrect_secret_key": PresignedAuth_incorrect_secret_key, + "PresignedAuth_PutObject_success": PresignedAuth_PutObject_success, + "PresignedAuth_Put_GetObject_with_data": PresignedAuth_Put_GetObject_with_data, "CreateBucket_invalid_bucket_name": CreateBucket_invalid_bucket_name, "CreateBucket_existing_bucket": CreateBucket_existing_bucket, "CreateBucket_as_user": CreateBucket_as_user, diff --git a/integration/tests.go b/integration/tests.go index 0a7a778e9..16ac4a3f3 100644 --- a/integration/tests.go +++ b/integration/tests.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "net/url" "regexp" "strings" "time" @@ -19,7 +20,8 @@ import ( ) var ( - shortTimeout = 10 * time.Second + shortTimeout = 10 * time.Second + iso8601Format = "20060102T150405Z" ) func Authentication_empty_auth_header(s *S3Conf) error { @@ -624,6 +626,843 @@ func Authentication_signature_error_incorrect_secret_key(s *S3Conf) error { }) } +func PresignedAuth_missing_algo_query_param(s *S3Conf) error { + testName := "PresignedAuth_missing_algo_query_param" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Del("X-Amz-Algorithm") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_unsupported_algorithm(s *S3Conf) error { + testName := "PresignedAuth_unsupported_algorithm" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + uri := strings.Replace(v4req.URL, "AWS4-HMAC-SHA256", "AWS4-SHA256", 1) + + req, err := http.NewRequest(v4req.Method, uri, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQuerySignatureAlgo)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_missing_credentials_query_param(s *S3Conf) error { + testName := "PresignedAuth_missing_credentials_query_param" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Del("X-Amz-Credential") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_malformed_creds_invalid_parts(s *S3Conf) error { + testName := "PresignedAuth_malformed_creds_invalid_parts" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Set("X-Amz-Credential", "access/hello/world") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrCredMalformed)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_creds_invalid_terminator(s *S3Conf) error { + testName := "PresignedAuth_creds_invalid_terminator" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + uri, err := changeAuthCred(v4req.URL, "aws5_request", credTerminator) + if err != nil { + return err + } + + req, err := http.NewRequest(v4req.Method, uri, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureTerminationStr)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_creds_incorrect_service(s *S3Conf) error { + testName := "PresignedAuth_creds_incorrect_service" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + uri, err := changeAuthCred(v4req.URL, "sns", credService) + if err != nil { + return err + } + + req, err := http.NewRequest(v4req.Method, uri, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureIncorrService)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_creds_incorrect_region(s *S3Conf) error { + testName := "PresignedAuth_creds_incorrect_region" + cfg := *s + + if cfg.awsRegion == "us-east-1" { + cfg.awsRegion = "us-west-1" + } else { + cfg.awsRegion = "us-east-1" + } + + return presignedAuthHandler(&cfg, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + req, err := http.NewRequest(v4req.Method, v4req.URL, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.APIError{ + Code: "SignatureDoesNotMatch", + Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", cfg.awsRegion), + HTTPStatusCode: http.StatusForbidden, + }); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_creds_invalid_date(s *S3Conf) error { + testName := "PresignedAuth_creds_invalid_date" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + uri, err := changeAuthCred(v4req.URL, "32234Z34", credDate) + if err != nil { + return err + } + + req, err := http.NewRequest(v4req.Method, uri, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_non_existing_access_key_id(s *S3Conf) error { + testName := "PresignedAuth_non_existing_access_key_id" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + uri, err := changeAuthCred(v4req.URL, "a_rarely_existing_access_key_id890asd6f807as6ydf870say", credAccess) + if err != nil { + return err + } + + req, err := http.NewRequest(v4req.Method, uri, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidAccessKeyID)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_missing_date_query(s *S3Conf) error { + testName := "PresignedAuth_missing_date_query" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Del("X-Amz-Date") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_dates_mismatch(s *S3Conf) error { + testName := "PresignedAuth_dates_mismatch" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + uri, err := changeAuthCred(v4req.URL, "20060102", credDate) + if err != nil { + return err + } + + req, err := http.NewRequest(v4req.Method, uri, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_missing_signed_headers_query_param(s *S3Conf) error { + testName := "PresignedAuth_missing_signed_headers_query_param" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Del("X-Amz-SignedHeaders") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_missing_expiration_query_param(s *S3Conf) error { + testName := "PresignedAuth_missing_expiration_query_param" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Del("X-Amz-Expires") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrInvalidQueryParams)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_invalid_expiration_query_param(s *S3Conf) error { + testName := "PresignedAuth_invalid_expiration_query_param" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Set("X-Amz-Expires", "invalid_value") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMalformedExpires)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_negative_expiration_query_param(s *S3Conf) error { + testName := "PresignedAuth_negative_expiration_query_param" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Set("X-Amz-Expires", "-3") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrNegativeExpires)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_exceeding_expiration_query_param(s *S3Conf) error { + testName := "PresignedAuth_exceeding_expiration_query_param" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + queries := urlParsed.Query() + queries.Set("X-Amz-Expires", "60580000") + urlParsed.RawQuery = queries.Encode() + + req, err := http.NewRequest(v4req.Method, urlParsed.String(), nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrMaximumExpires)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_expired_request(s *S3Conf) error { + testName := "PresignedAuth_expired_request" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + urlParsed, err := url.Parse(v4req.URL) + if err != nil { + return err + } + + expDate := time.Now().AddDate(0, -1, 0).Format(iso8601Format) + + queries := urlParsed.Query() + queries.Set("X-Amz-Date", expDate) + urlParsed.RawQuery = queries.Encode() + + uri, err := changeAuthCred(urlParsed.String(), expDate[:8], credDate) + if err != nil { + return err + } + + req, err := http.NewRequest(v4req.Method, uri, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrExpiredPresignRequest)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_incorrect_secret_key(s *S3Conf) error { + testName := "PresignedAuth_incorrect_secret_key" + cfg := *s + cfg.awsSecret += "x" + return presignedAuthHandler(&cfg, testName, func(client *s3.PresignClient) error { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignDeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: getPtr("my-bucket")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + req, err := http.NewRequest(v4req.Method, v4req.URL, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if err := checkAuthErr(resp, s3err.GetAPIError(s3err.ErrSignatureDoesNotMatch)); err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_PutObject_success(s *S3Conf) error { + testName := "PresignedAuth_PutObject_success" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + bucket := getBucketName() + err := setup(s, bucket) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignPutObject(ctx, &s3.PutObjectInput{Bucket: &bucket, Key: getPtr("my-obj")}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + req, err := http.NewRequest(http.MethodPut, v4req.URL, nil) + if err != nil { + return err + } + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected my-obj to be successfully uploaded and get 200 response status, instead got %v", resp.StatusCode) + } + + err = teardown(s, bucket) + if err != nil { + return err + } + + return nil + }) +} + +func PresignedAuth_Put_GetObject_with_data(s *S3Conf) error { + testName := "PresignedAuth_Put_GetObject_with_data" + return presignedAuthHandler(s, testName, func(client *s3.PresignClient) error { + bucket, obj := getBucketName(), "my-obj" + err := setup(s, bucket) + if err != nil { + return err + } + + data := "Hello world" + body := strings.NewReader(data) + + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + v4req, err := client.PresignPutObject(ctx, &s3.PutObjectInput{Bucket: &bucket, Key: &obj, Body: body}) + cancel() + if err != nil { + return err + } + + httpClient := http.Client{ + Timeout: shortTimeout, + } + + req, err := http.NewRequest(v4req.Method, v4req.URL, body) + if err != nil { + return err + } + + req.Header = v4req.SignedHeader + + resp, err := httpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected my-obj to be successfully uploaded and get %v response status, instead got %v", http.StatusOK, resp.StatusCode) + } + + ctx, cancel = context.WithTimeout(context.Background(), shortTimeout) + v4GetReq, err := client.PresignGetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: &obj}) + cancel() + if err != nil { + return err + } + + req, err = http.NewRequest(v4GetReq.Method, v4GetReq.URL, nil) + if err != nil { + return err + } + + resp, err = httpClient.Do(req) + if err != nil { + return err + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("expected get object response status to be %v, instead got %v", http.StatusOK, resp.StatusCode) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read get object response body %w", err) + } + + fmt.Println(resp.Request.Method, resp.ContentLength, string(respBody)) + + if string(respBody) != data { + return fmt.Errorf("expected get object response body to be %v, instead got %s", data, respBody) + } + + err = teardown(s, bucket) + if err != nil { + return err + } + + return nil + }) +} + func CreateBucket_invalid_bucket_name(s *S3Conf) error { testName := "CreateBucket_invalid_bucket_name" runF(testName) diff --git a/integration/utils.go b/integration/utils.go index 014276cd6..1a6912701 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -12,6 +12,7 @@ import ( "io" rnd "math/rand" "net/http" + "net/url" "os" "os/exec" "strings" @@ -150,6 +151,20 @@ func authHandler(s *S3Conf, cfg *authConfig, handler func(req *http.Request) err return nil } +func presignedAuthHandler(s *S3Conf, testName string, handler func(client *s3.PresignClient) error) error { + runF(testName) + clt := s3.NewPresignClient(s3.NewFromConfig(s.Config())) + + err := handler(clt) + if err != nil { + failF("%v: %v", testName, err) + return fmt.Errorf("%v: %w", testName, err) + } + + passF(testName) + return nil +} + func createSignedReq(method, endpoint, path, access, secret, service, region string, body []byte, date time.Time) (*http.Request, error) { req, err := http.NewRequest(method, fmt.Sprintf("%v/%v", endpoint, path), bytes.NewReader(body)) if err != nil { @@ -551,3 +566,26 @@ func genRandString(length int) string { } return string(result) } + +const ( + credAccess int = iota + credDate + credRegion + credService + credTerminator +) + +func changeAuthCred(uri, newVal string, index int) (string, error) { + urlParsed, err := url.Parse(uri) + if err != nil { + return "", err + } + + queries := urlParsed.Query() + creds := strings.Split(queries.Get("X-Amz-Credential"), "/") + creds[index] = newVal + queries.Set("X-Amz-Credential", strings.Join(creds, "/")) + urlParsed.RawQuery = queries.Encode() + + return urlParsed.String(), nil +} diff --git a/s3api/middlewares/presign-auth.go b/s3api/middlewares/presign-auth.go index b789eb1cf..bed9dda3f 100644 --- a/s3api/middlewares/presign-auth.go +++ b/s3api/middlewares/presign-auth.go @@ -64,6 +64,6 @@ func VerifyPresignedV4Signature(root RootUserConfig, iam auth.IAMService, logger return sendResponse(ctx, err, logger) } - return nil + return ctx.Next() } } diff --git a/s3api/utils/presign-auth-reader.go b/s3api/utils/presign-auth-reader.go index 2f2892f68..fd2938be1 100644 --- a/s3api/utils/presign-auth-reader.go +++ b/s3api/utils/presign-auth-reader.go @@ -21,6 +21,7 @@ import ( "net/http" "net/url" "os" + "strconv" "strings" "time" @@ -73,14 +74,24 @@ func (pr *PresignedAuthReader) Read(p []byte) (int, error) { // CheckPresignedSignature validates presigned request signature func CheckPresignedSignature(ctx *fiber.Ctx, auth AuthData, secret string, debug bool) error { + signedHdrs := strings.Split(auth.SignedHeaders, ";") + + var contentLength int64 + var err error + contentLengthStr := ctx.Get("Content-Length") + if contentLengthStr != "" { + contentLength, err = strconv.ParseInt(contentLengthStr, 10, 64) + if err != nil { + return s3err.GetAPIError(s3err.ErrInvalidRequest) + } + } + // Create a new http request instance from fasthttp request - req, err := createPresignedHttpRequestFromCtx(ctx) + req, err := createPresignedHttpRequestFromCtx(ctx, signedHdrs, contentLength) if err != nil { return fmt.Errorf("create http request from context: %w", err) } - fmt.Println("http request has been created") - date, _ := time.Parse(iso8601Format, auth.Date) signer := v4.NewSigner() @@ -90,10 +101,8 @@ func CheckPresignedSignature(ctx *fiber.Ctx, auth AuthData, secret string, debug }, req, unsignedPayload, service, auth.Region, date, func(options *v4.SignerOptions) { options.DisableURIPathEscaping = true if debug { - if debug { - options.LogSigning = true - options.Logger = logging.NewStandardLogger(os.Stderr) - } + options.LogSigning = true + options.Logger = logging.NewStandardLogger(os.Stderr) } }) if signErr != nil { @@ -128,14 +137,17 @@ func ParsePresignedURIParts(ctx *fiber.Ctx) (AuthData, error) { // Get and verify algorithm query parameter algo := ctx.Query("X-Amz-Algorithm") + if algo == "" { + return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams) + } if algo != "AWS4-HMAC-SHA256" { - return a, s3err.GetAPIError(s3err.ErrSignatureVersionNotSupported) + return a, s3err.GetAPIError(s3err.ErrInvalidQuerySignatureAlgo) } // Parse and validate credentials query parameter credsQuery := ctx.Query("X-Amz-Credential") if credsQuery == "" { - return a, s3err.GetAPIError(s3err.ErrCredMalformed) + return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams) } creds := strings.Split(credsQuery, "/") @@ -156,7 +168,7 @@ func ParsePresignedURIParts(ctx *fiber.Ctx) (AuthData, error) { // Parse and validate Date query param date := ctx.Query("X-Amz-Date") if date == "" { - return a, s3err.GetAPIError(s3err.ErrMissingDateHeader) + return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams) } tdate, err := time.Parse(iso8601Format, date) @@ -168,25 +180,64 @@ func ParsePresignedURIParts(ctx *fiber.Ctx) (AuthData, error) { return a, s3err.GetAPIError(s3err.ErrSignatureDateDoesNotMatch) } - err = ValidateDate(tdate) - if err != nil { - return a, err - } - if ctx.Locals("region") != creds[2] { return a, s3err.APIError{ Code: "SignatureDoesNotMatch", - Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", ctx.Locals("region")), + Description: fmt.Sprintf("Credential should be scoped to a valid Region, not %v", creds[2]), HTTPStatusCode: http.StatusForbidden, } } - a.Signature = ctx.Query("X-Amz-Signature") + signature := ctx.Query("X-Amz-Signature") + if signature == "" { + return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams) + } + + signedHdrs := ctx.Query("X-Amz-SignedHeaders") + if signedHdrs == "" { + return a, s3err.GetAPIError(s3err.ErrInvalidQueryParams) + } + + // Validate X-Amz-Expires query param and check if request is expired + err = validateExpiration(ctx.Query("X-Amz-Expires"), tdate) + if err != nil { + return a, err + } + + a.Signature = signature a.Access = creds[0] a.Algorithm = algo a.Region = creds[2] - a.SignedHeaders = ctx.Query("X-Amz-SignedHeaders") + a.SignedHeaders = signedHdrs a.Date = date return a, nil } + +func validateExpiration(str string, date time.Time) error { + if str == "" { + return s3err.GetAPIError(s3err.ErrInvalidQueryParams) + } + + exp, err := strconv.Atoi(str) + if err != nil { + return s3err.GetAPIError(s3err.ErrMalformedExpires) + } + + if exp < 0 { + return s3err.GetAPIError(s3err.ErrNegativeExpires) + } + + if exp > 604800 { + return s3err.GetAPIError(s3err.ErrMaximumExpires) + } + + now := time.Now() + passed := int(now.Sub(date).Seconds()) + + if passed > exp { + return s3err.GetAPIError(s3err.ErrExpiredPresignRequest) + } + + return nil +} diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index f4a4d1bc6..71bd9b4a7 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -97,7 +97,7 @@ var ( } ) -func createPresignedHttpRequestFromCtx(ctx *fiber.Ctx) (*http.Request, error) { +func createPresignedHttpRequestFromCtx(ctx *fiber.Ctx, signedHdrs []string, contentLength int64) (*http.Request, error) { req := ctx.Request() var body io.Reader if IsBigDataAction(ctx) { @@ -125,6 +125,21 @@ func createPresignedHttpRequestFromCtx(ctx *fiber.Ctx) (*http.Request, error) { if err != nil { return nil, errors.New("error in creating an http request") } + // Set the request headers + req.Header.VisitAll(func(key, value []byte) { + keyStr := string(key) + if includeHeader(keyStr, signedHdrs) { + httpReq.Header.Add(keyStr, string(value)) + } + }) + + // Check if Content-Length in signed headers + // If content length is non 0, then the header will be included + if !includeHeader("Content-Length", signedHdrs) { + httpReq.ContentLength = 0 + } else { + httpReq.ContentLength = contentLength + } // Set the Host header httpReq.Host = string(req.Header.Host())