fix url encoding when validating pre-signed signature
diff --git a/localstack/services/s3/presigned_url.py b/localstack/services/s3/presigned_url.py
index faea038fee49e..f6b9bde367b15 100644
--- a/localstack/services/s3/presigned_url.py
+++ b/localstack/services/s3/presigned_url.py
@@ -282,6 +282,7 @@ def validate_presigned_url_s3(context: RequestContext) -> None:
     :param context: RequestContext
     """
     query_parameters = context.request.args
+    method = context.request.method
     # todo: use the current User credentials instead? so it would not be set in stone??
     credentials = Credentials(
         access_key=TEST_AWS_ACCESS_KEY_ID,
@@ -306,13 +307,10 @@ def validate_presigned_url_s3(context: RequestContext) -> None:
 
     auth_signer = HmacV1QueryAuthValidation(credentials=credentials, expires=expires)
 
-    pre_signature_request = _reverse_inject_signature_hmac_v1_query(context)
-
-    split = urlsplit(pre_signature_request.url)
-    headers = _get_aws_request_headers(pre_signature_request.headers)
+    split_url, headers = _reverse_inject_signature_hmac_v1_query(context.request)
 
     signature, string_to_sign = auth_signer.get_signature(
-        pre_signature_request.method, split, headers, auth_path=None
+        method, split_url, headers, auth_path=None
     )
     # after passing through the virtual host to path proxy, the signature is parsed and `+` are replaced by space
     req_signature = context.request.args.get("Signature").replace(" ", "+")
@@ -329,36 +327,20 @@ def validate_presigned_url_s3(context: RequestContext) -> None:
             raise ex
 
 
-def _get_aws_request_headers(werkzeug_headers: Headers) -> HTTPHeaders:
-    """
-    Converts Werkzeug headers into HTTPHeaders() needed to form an AWSRequest
-    :param werkzeug_headers: Werkzeug request headers
-    :return: headers in HTTPHeaders format
-    """
-    # Werkzeug Headers can have multiple values for the same key
-    # HTTPHeaders will append automatically the values when we set it to the same key multiple times
-    # see https://docs.python.org/3/library/http.client.html#httpmessage-objects
-    # see https://docs.python.org/3/library/email.compat32-message.html#email.message.Message.__setitem__
-    headers = HTTPHeaders()
-    for key, value in werkzeug_headers.items():
-        headers[key] = value
-
-    return headers
-
-
-def _reverse_inject_signature_hmac_v1_query(context: RequestContext) -> Request:
+def _reverse_inject_signature_hmac_v1_query(
+    request: Request,
+) -> tuple[urlparse.SplitResult, HTTPHeaders]:
     """
     Reverses what does HmacV1QueryAuth._inject_signature while injecting the signature in the request.
     Transforms the query string parameters in headers to recalculate the signature
     see botocore.auth.HmacV1QueryAuth._inject_signature
-    :param context:
-    :return:
+    :param request: the original request
+    :return: tuple of a split result from the reversed request, and the reversed headers
     """
-
     new_headers = {}
     new_query_string_dict = {}
 
-    for header, value in context.request.args.items():
+    for header, value in request.args.items():
         header_low = header.lower()
         if header_low not in HmacV1QueryAuthValidation.post_signature_headers:
             new_headers[header] = value
@@ -367,7 +349,7 @@ def _reverse_inject_signature_hmac_v1_query(context: RequestContext) -> Request:
 
     # there should not be any headers here. If there are, it means they have been added by the client
     # We should verify them, they will fail the signature except if they were part of the original request
-    for header, value in context.request.headers.items():
+    for header, value in request.headers.items():
         header_low = header.lower()
         if header_low.startswith("x-amz-") or header_low in ["content-type", "date", "content-md5"]:
             new_headers[header_low] = value
@@ -375,36 +357,16 @@ def _reverse_inject_signature_hmac_v1_query(context: RequestContext) -> Request:
     # rebuild the query string
     new_query_string = percent_encode_sequence(new_query_string_dict)
 
-    # easier to recreate the request, we would have to delete every cached property otherwise
-    reversed_request = _create_new_request(
-        request=context.request,
-        headers=new_headers,
-        query_string=new_query_string,
-    )
+    # we need to URL encode the path, as the key needs to be urlencoded for the signature to match
+    encoded_path = urlparse.quote(request.path)
 
-    return reversed_request
+    reversed_url = f"{request.scheme}://{request.host}{encoded_path}?{new_query_string}"
 
+    reversed_headers = HTTPHeaders()
+    for key, value in new_headers.items():
+        reversed_headers[key] = value
 
-def _create_new_request(request: Request, headers: Dict[str, str], query_string: str) -> Request:
-    """
-    Create a new request from an existent one, with new headers and query string
-    It is easier to create a new one as the existing request has a lot of cached properties based on query_string
-    We are not using the request body to generate the signature, so do not pass it to the new request
-    :param request: the incoming pre-signed request
-    :param headers: new headers used for signature calculation
-    :param query_string: new query string for signature calculation
-    :return: a new Request with passed headers and query_string
-    """
-    return Request(
-        method=request.method,
-        headers=headers,
-        path=request.path,
-        query_string=query_string,
-        scheme=request.scheme,
-        root_path=request.root_path,
-        server=request.server,
-        remote_addr=request.remote_addr,
-    )
+    return urlsplit(reversed_url), reversed_headers
 
 
 def validate_presigned_url_s3v4(context: RequestContext) -> None:
@@ -537,6 +499,8 @@ def __init__(self, context: RequestContext):
             else:
                 self.path = self.request.path
 
+        # we need to URL encode the path, as the key needs to be urlencoded for the signature to match
+        self.path = urlparse.quote(self.path)
         self.aws_request = self._get_aws_request()
 
     def update_host_port(self, new_host_port: str, original_host_port: str = None):
diff --git a/tests/integration/s3/test_s3.py b/tests/integration/s3/test_s3.py
index 9cb4481b6b824..924733e04c36d 100644
--- a/tests/integration/s3/test_s3.py
+++ b/tests/integration/s3/test_s3.py
@@ -6496,6 +6496,31 @@ def test_pre_signed_url_forward_slash_bucket(
         request_content = xmltodict.parse(req.content)
         assert "GET\n//test-bucket" in request_content["Error"]["CanonicalRequest"]
 
+    @pytest.mark.parametrize(
+        "signature_version",
+        ["s3", "s3v4"],
+    )
+    def test_s3_presign_url_encoding(
+        self, aws_client, s3_bucket, signature_version, patch_s3_skip_signature_validation_false
+    ):
+        object_key = "table1-partitioned/date=2023-06-28/test.csv"
+        aws_client.s3.put_object(Key=object_key, Bucket=s3_bucket, Body="123")
+
+        s3_endpoint_path_style = _endpoint_url()
+        client = _s3_client_custom_config(
+            Config(signature_version=signature_version, s3={}),
+            endpoint_url=s3_endpoint_path_style,
+        )
+
+        url = client.generate_presigned_url(
+            "get_object",
+            Params={"Bucket": s3_bucket, "Key": object_key},
+        )
+
+        req = requests.get(url)
+        assert req.ok
+        assert req.content == b"123"
+
     @staticmethod
     def _get_presigned_snapshot_transformers(snapshot):
         return [

As reported by #8663, we were not properly re-URL-encoding the request path before validating the signature of the pre-signed URL.

I've made the necessary modifications into both signature types, and simplified the HMACv1 (signature type `s3`) quite a bit by not recreating a `Request` object but instead directly recreating a `SplitResult` and the new `HTTPHeaders`. 

Added a test to validate the use case of the user and the fix. 