Repository: getmoto/moto
Pull request #6728 (2023-08-26 01:34:04)

KMS: Add fail test cases for signing algorithms of RSA
Adding tests to check that the verification fails if a different algorithm is used than the one used when signing.


Patch:
diff --git a/moto/core/responses.py b/moto/core/responses.py
index bfdbac1e8..8e97b8f42 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -14,6 +14,7 @@ from moto.core.common_types import TYPE_RESPONSE, TYPE_IF_NONE
 from moto.core.exceptions import DryRunClientError
 from moto.core.utils import (
     camelcase_to_underscores,
+    gzip_decompress,
     method_names_from_class,
     params_sort_function,
 )
@@ -232,6 +233,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
     def __init__(self, service_name: Optional[str] = None):
         super().__init__()
         self.service_name = service_name
+        self.allow_request_decompression = True
 
     @classmethod
     def dispatch(cls, *args: Any, **kwargs: Any) -> Any:  # type: ignore[misc]
@@ -262,6 +264,15 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
                 querystring[key] = [value]
 
         raw_body = self.body
+
+        # https://github.com/getmoto/moto/issues/6692
+        # Content coming from SDK's can be GZipped for performance reasons
+        if (
+            headers.get("Content-Encoding", "") == "gzip"
+            and self.allow_request_decompression
+        ):
+            self.body = gzip_decompress(self.body)
+
         if isinstance(self.body, bytes) and not use_raw_body:
             self.body = self.body.decode("utf-8")
 
diff --git a/moto/core/utils.py b/moto/core/utils.py
index 813c4ad47..483ce1af7 100644
--- a/moto/core/utils.py
+++ b/moto/core/utils.py
@@ -3,6 +3,7 @@ import inspect
 import re
 import unicodedata
 from botocore.exceptions import ClientError
+from gzip import decompress
 from typing import Any, Optional, List, Callable, Dict, Tuple
 from urllib.parse import urlparse, unquote
 from .common_types import TYPE_RESPONSE
@@ -416,3 +417,7 @@ def _unquote_hex_characters(path: str) -> str:
         )
         char_offset += (combo_end - combo_start) + len(character) - 1
     return path
+
+
+def gzip_decompress(body: bytes) -> bytes:
+    return decompress(body)
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 48652f3d7..10020677b 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -220,6 +220,10 @@ class Cluster:
             or self.replication_source_identifier is not None
         )
 
+    @property
+    def arn(self) -> str:
+        return self.db_cluster_arn
+
     @property
     def db_cluster_arn(self) -> str:
         return f"arn:aws:rds:{self.region_name}:{self.account_id}:cluster:{self.db_cluster_identifier}"
@@ -461,6 +465,10 @@ class ClusterSnapshot(BaseModel):
             datetime.datetime.utcnow()
         )
 
+    @property
+    def arn(self) -> str:
+        return self.snapshot_arn
+
     @property
     def snapshot_arn(self) -> str:
         return f"arn:aws:rds:{self.cluster.region_name}:{self.cluster.account_id}:cluster-snapshot:{self.snapshot_id}"
@@ -645,6 +653,10 @@ class Database(CloudFormationModel):
             kwargs.get("enable_cloudwatch_logs_exports") or []
         )
 
+    @property
+    def arn(self) -> str:
+        return self.db_instance_arn
+
     @property
     def db_instance_arn(self) -> str:
         return f"arn:aws:rds:{self.region_name}:{self.account_id}:db:{self.db_instance_identifier}"
@@ -1091,6 +1103,10 @@ class DatabaseSnapshot(BaseModel):
             datetime.datetime.utcnow()
         )
 
+    @property
+    def arn(self) -> str:
+        return self.snapshot_arn
+
     @property
     def snapshot_arn(self) -> str:
         return f"arn:aws:rds:{self.database.region_name}:{self.database.account_id}:snapshot:{self.snapshot_id}"
diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py
index 48047dc50..67a03e2db 100644
--- a/moto/resourcegroupstaggingapi/models.py
+++ b/moto/resourcegroupstaggingapi/models.py
@@ -2,6 +2,7 @@ from typing import Any, Dict, List, Iterator, Optional, Tuple
 from moto.core import BaseBackend, BackendDict
 from moto.core.exceptions import RESTError
 from moto.moto_api._internal import mock_random
+from moto.utilities.tagging_service import TaggingService
 
 from moto.s3.models import s3_backends, S3Backend
 from moto.ec2 import ec2_backends
@@ -370,71 +371,32 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
 
                 yield {"ResourceARN": f"{kms_key.arn}", "Tags": tags}
 
-        # RDS Cluster
-        if (
-            not resource_type_filters
-            or "rds" in resource_type_filters
-            or "rds:cluster" in resource_type_filters
-        ):
-            for rds_cluster in self.rds_backend.clusters.values():
-                tags = rds_cluster.get_tags()
-                if not tags or not tag_filter(tags):
-                    continue
-                yield {
-                    "ResourceARN": rds_cluster.db_cluster_arn,
-                    "Tags": tags,
-                }
-
-        # RDS Instance
-        if (
-            not resource_type_filters
-            or "rds" in resource_type_filters
-            or "rds:db" in resource_type_filters
-        ):
-            for database in self.rds_backend.databases.values():
-                tags = database.get_tags()
-                if not tags or not tag_filter(tags):
-                    continue
-                yield {
-                    "ResourceARN": database.db_instance_arn,
-                    "Tags": tags,
-                }
+        # RDS resources
+        resource_map: Dict[str, Dict[str, Any]] = {
+            "rds:cluster": self.rds_backend.clusters,
+            "rds:db": self.rds_backend.databases,
+            "rds:snapshot": self.rds_backend.database_snapshots,
+            "rds:cluster-snapshot": self.rds_backend.cluster_snapshots,
+        }
+        for resource_type, resource_source in resource_map.items():
+            if (
+                not resource_type_filters
+                or "rds" in resource_type_filters
+                or resource_type in resource_type_filters
+            ):
+                for resource in resource_source.values():
+                    tags = resource.get_tags()
+                    if not tags or not tag_filter(tags):
+                        continue
+                    yield {
+                        "ResourceARN": resource.arn,
+                        "Tags": tags,
+                    }
 
         # RDS Reserved Database Instance
         # RDS Option Group
         # RDS Parameter Group
         # RDS Security Group
-
-        # RDS Snapshot
-        if (
-            not resource_type_filters
-            or "rds" in resource_type_filters
-            or "rds:snapshot" in resource_type_filters
-        ):
-            for snapshot in self.rds_backend.database_snapshots.values():
-                tags = snapshot.get_tags()
-                if not tags or not tag_filter(tags):
-                    continue
-                yield {
-                    "ResourceARN": snapshot.snapshot_arn,
-                    "Tags": tags,
-                }
-
-        # RDS Cluster Snapshot
-        if (
-            not resource_type_filters
-            or "rds" in resource_type_filters
-            or "rds:cluster-snapshot" in resource_type_filters
-        ):
-            for snapshot in self.rds_backend.cluster_snapshots.values():
-                tags = snapshot.get_tags()
-                if not tags or not tag_filter(tags):
-                    continue
-                yield {
-                    "ResourceARN": snapshot.snapshot_arn,
-                    "Tags": tags,
-                }
-
         # RDS Subnet Group
         # RDS Event Subscription
 
@@ -767,14 +729,27 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend):
 
         return new_token, result
 
-    # These methods will be called from responses.py.
-    # They should call a tag function inside of the moto module
-    # that governs the resource, that way if the target module
-    # changes how tags are delt with theres less to change
+    def tag_resources(
+        self, resource_arns: List[str], tags: Dict[str, str]
+    ) -> Dict[str, Dict[str, Any]]:
+        """
+        Only RDS resources are currently supported
+        """
+        missing_resources = []
+        missing_error: Dict[str, Any] = {
+            "StatusCode": 404,
+            "ErrorCode": "InternalServiceException",
+            "ErrorMessage": "Service not yet supported",
+        }
+        for arn in resource_arns:
+            if arn.startswith("arn:aws:rds:"):
+                self.rds_backend.add_tags_to_resource(
+                    arn, TaggingService.convert_dict_to_tags_input(tags)
+                )
+            else:
+                missing_resources.append(arn)
+        return {arn: missing_error for arn in missing_resources}
 
-    # def tag_resources(self, resource_arn_list, tags):
-    #     return failed_resources_map
-    #
     # def untag_resources(self, resource_arn_list, tag_keys):
     #     return failed_resources_map
 
diff --git a/moto/resourcegroupstaggingapi/responses.py b/moto/resourcegroupstaggingapi/responses.py
index 190ae4337..dbe95e70a 100644
--- a/moto/resourcegroupstaggingapi/responses.py
+++ b/moto/resourcegroupstaggingapi/responses.py
@@ -58,21 +58,15 @@ class ResourceGroupsTaggingAPIResponse(BaseResponse):
 
         return json.dumps(response)
 
-    # These methods are all thats left to be implemented
-    # the response is already set up, all thats needed is
-    # the respective model function to be implemented.
-    #
-    # def tag_resources(self):
-    #     resource_arn_list = self._get_list_prefix("ResourceARNList.member")
-    #     tags = self._get_param("Tags")
-    #     failed_resources_map = self.backend.tag_resources(
-    #         resource_arn_list=resource_arn_list,
-    #         tags=tags,
-    #     )
-    #
-    #     # failed_resources_map should be {'resource': {'ErrorCode': str, 'ErrorMessage': str, 'StatusCode': int}}
-    #     return json.dumps({'FailedResourcesMap': failed_resources_map})
-    #
+    def tag_resources(self) -> str:
+        resource_arns = self._get_param("ResourceARNList")
+        tags = self._get_param("Tags")
+        failed_resources = self.backend.tag_resources(
+            resource_arns=resource_arns, tags=tags
+        )
+
+        return json.dumps({"FailedResourcesMap": failed_resources})
+
     # def untag_resources(self):
     #     resource_arn_list = self._get_list_prefix("ResourceARNList.member")
     #     tag_keys = self._get_list_prefix("TagKeys.member")
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index e60027b21..701400b72 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -158,6 +158,11 @@ def parse_key_name(pth: str) -> str:
 class S3Response(BaseResponse):
     def __init__(self) -> None:
         super().__init__(service_name="s3")
+        # Whatever format requests come in, we should never touch them
+        # There are some nuances here - this decision should be method-specific, instead of service-specific
+        # E.G.: we don't want to touch put_object(), but we might have to decompress put_object_configuration()
+        # Taking the naive approach to never decompress anything from S3 for now
+        self.allow_request_decompression = False
 
     def get_safe_path_from_url(self, url: ParseResult) -> str:
         return self.get_safe_path(url.path)


Test patch:
diff --git a/tests/test_core/test_responses.py b/tests/test_core/test_responses.py
index 46ca6a45d..d33edb8ce 100644
--- a/tests/test_core/test_responses.py
+++ b/tests/test_core/test_responses.py
@@ -2,11 +2,13 @@ import datetime
 from unittest import SkipTest, mock
 
 from collections import OrderedDict
+from gzip import compress as gzip_compress
 
 from botocore.awsrequest import AWSPreparedRequest
 
 from moto.core.responses import AWSServiceSpec, BaseResponse
 from moto.core.responses import flatten_json_request_body
+from moto.s3.responses import S3Response
 from moto import settings
 from freezegun import freeze_time
 
@@ -244,3 +246,38 @@ def test_response_metadata():
     assert "date" in bc.response_headers
     if not settings.TEST_SERVER_MODE:
         assert bc.response_headers["date"] == "Sat, 20 May 2023 10:20:30 GMT"
+
+
+def test_compression_gzip():
+    body = '{"key": "%D0"}, "C": "#0 = :0"}'
+    request = AWSPreparedRequest(
+        "GET",
+        url="http://request",
+        headers={"Content-Encoding": "gzip"},
+        body=_gzip_compress_body(body),
+        stream_output=False,
+    )
+    response = BaseResponse()
+    response.setup_class(request, request.url, request.headers)
+
+    assert body == response.body
+
+
+def test_compression_gzip_in_s3():
+    body = b"some random data"
+    request = AWSPreparedRequest(
+        "GET",
+        url="http://request",
+        headers={"Content-Encoding": "gzip"},
+        body=body,
+        stream_output=False,
+    )
+    response = S3Response()
+    response.setup_class(request, request.url, request.headers)
+
+    assert body == response.body.encode("utf-8")
+
+
+def _gzip_compress_body(body: str):
+    assert isinstance(body, str)
+    return gzip_compress(data=body.encode("utf-8"))
diff --git a/tests/test_kms/test_kms_boto3.py b/tests/test_kms/test_kms_boto3.py
index ca2b75241..ffb81571c 100644
--- a/tests/test_kms/test_kms_boto3.py
+++ b/tests/test_kms/test_kms_boto3.py
@@ -1204,6 +1204,47 @@ def test_sign_and_verify_digest_message_type_RSA(key_spec, signing_algorithm):
     assert verify_response["SignatureValid"] is True
 
 
+@mock_kms
+@pytest.mark.parametrize(
+    "signing_algorithm, another_signing_algorithm",
+    list(
+        itertools.combinations(
+            ["RSASSA_PSS_SHA_256", "RSASSA_PSS_SHA_384", "RSASSA_PSS_SHA_512"], 2
+        )
+    ),
+)
+def test_fail_verify_digest_message_type_RSA(
+    signing_algorithm, another_signing_algorithm
+):
+    client = boto3.client("kms", region_name="us-west-1")
+
+    key = client.create_key(
+        Description="sign-key", KeyUsage="SIGN_VERIFY", KeySpec="RSA_2048"
+    )
+    key_id = key["KeyMetadata"]["KeyId"]
+
+    digest = hashes.Hash(hashes.SHA256())
+    digest.update(b"this works")
+    digest.update(b"as well")
+    message = digest.finalize()
+
+    sign_response = client.sign(
+        KeyId=key_id,
+        Message=message,
+        SigningAlgorithm=signing_algorithm,
+        MessageType="DIGEST",
+    )
+
+    verify_response = client.verify(
+        KeyId=key_id,
+        Message=message,
+        Signature=sign_response["Signature"],
+        SigningAlgorithm=another_signing_algorithm,
+    )
+
+    assert verify_response["SignatureValid"] is False
+
+
 @mock_kms
 def test_sign_invalid_key_usage():
     client = boto3.client("kms", region_name="us-west-2")
diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_rds.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_rds.py
new file mode 100644
index 000000000..d46069c71
--- /dev/null
+++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstagging_rds.py
@@ -0,0 +1,71 @@
+import boto3
+import unittest
+
+from moto import mock_rds
+from moto import mock_resourcegroupstaggingapi
+
+
+@mock_rds
+@mock_resourcegroupstaggingapi
+class TestRdsTagging(unittest.TestCase):
+    def setUp(self) -> None:
+        self.rds = boto3.client("rds", region_name="us-west-2")
+        self.rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-2")
+        self.resources_tagged = []
+        self.resources_untagged = []
+        for i in range(3):
+            database = self.rds.create_db_instance(
+                DBInstanceIdentifier=f"db-instance-{i}",
+                Engine="postgres",
+                DBInstanceClass="db.m1.small",
+                CopyTagsToSnapshot=bool(i),
+                Tags=[{"Key": "test", "Value": f"value-{i}"}] if i else [],
+            ).get("DBInstance")
+            snapshot = self.rds.create_db_snapshot(
+                DBInstanceIdentifier=database["DBInstanceIdentifier"],
+                DBSnapshotIdentifier=f"snapshot-{i}",
+            ).get("DBSnapshot")
+            group = self.resources_tagged if i else self.resources_untagged
+            group.append(database["DBInstanceArn"])
+            group.append(snapshot["DBSnapshotArn"])
+
+    def test_get_resources_rds(self):
+        def assert_response(response, expected_count, resource_type=None):
+            results = response.get("ResourceTagMappingList", [])
+            assert len(results) == expected_count
+            for item in results:
+                arn = item["ResourceARN"]
+                assert arn in self.resources_tagged
+                assert arn not in self.resources_untagged
+                if resource_type:
+                    assert f":{resource_type}:" in arn
+
+        resp = self.rtapi.get_resources(ResourceTypeFilters=["rds"])
+        assert_response(resp, 4)
+        resp = self.rtapi.get_resources(ResourceTypeFilters=["rds:db"])
+        assert_response(resp, 2, resource_type="db")
+        resp = self.rtapi.get_resources(ResourceTypeFilters=["rds:snapshot"])
+        assert_response(resp, 2, resource_type="snapshot")
+        resp = self.rtapi.get_resources(
+            TagFilters=[{"Key": "test", "Values": ["value-1"]}]
+        )
+        assert_response(resp, 2)
+
+    def test_tag_resources_rds(self):
+        # WHEN
+        # we tag resources
+        self.rtapi.tag_resources(
+            ResourceARNList=self.resources_tagged,
+            Tags={"key1": "value1", "key2": "value2"},
+        )
+
+        # THEN
+        # we can retrieve the tags using the RDS API
+        def get_tags(arn):
+            return self.rds.list_tags_for_resource(ResourceName=arn)["TagList"]
+
+        for arn in self.resources_tagged:
+            assert {"Key": "key1", "Value": "value1"} in get_tags(arn)
+            assert {"Key": "key2", "Value": "value2"} in get_tags(arn)
+        for arn in self.resources_untagged:
+            assert get_tags(arn) == []
diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
index 7884b9ad7..95bcd15f7 100644
--- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
+++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py
@@ -6,7 +6,6 @@ from botocore.client import ClientError
 from moto import mock_ec2
 from moto import mock_elbv2
 from moto import mock_kms
-from moto import mock_rds
 from moto import mock_resourcegroupstaggingapi
 from moto import mock_s3
 from moto import mock_lambda
@@ -223,7 +222,6 @@ def test_get_resources_ecs():
     assert task_two not in resp["ResourceTagMappingList"][0]["ResourceARN"]
 
 
-@mock_rds
 @mock_ec2
 @mock_resourcegroupstaggingapi
 def test_get_resources_ec2():
@@ -588,49 +586,6 @@ def test_multiple_tag_filters():
     assert instance_2_id not in results[0]["ResourceARN"]
 
 
-@mock_rds
-@mock_resourcegroupstaggingapi
-def test_get_resources_rds():
-    client = boto3.client("rds", region_name="us-west-2")
-    resources_tagged = []
-    resources_untagged = []
-    for i in range(3):
-        database = client.create_db_instance(
-            DBInstanceIdentifier=f"db-instance-{i}",
-            Engine="postgres",
-            DBInstanceClass="db.m1.small",
-            CopyTagsToSnapshot=bool(i),
-            Tags=[{"Key": "test", "Value": f"value-{i}"}] if i else [],
-        ).get("DBInstance")
-        snapshot = client.create_db_snapshot(
-            DBInstanceIdentifier=database["DBInstanceIdentifier"],
-            DBSnapshotIdentifier=f"snapshot-{i}",
-        ).get("DBSnapshot")
-        group = resources_tagged if i else resources_untagged
-        group.append(database["DBInstanceArn"])
-        group.append(snapshot["DBSnapshotArn"])
-
-    def assert_response(response, expected_count, resource_type=None):
-        results = response.get("ResourceTagMappingList", [])
-        assert len(results) == expected_count
-        for item in results:
-            arn = item["ResourceARN"]
-            assert arn in resources_tagged
-            assert arn not in resources_untagged
-            if resource_type:
-                assert f":{resource_type}:" in arn
-
-    rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-2")
-    resp = rtapi.get_resources(ResourceTypeFilters=["rds"])
-    assert_response(resp, 4)
-    resp = rtapi.get_resources(ResourceTypeFilters=["rds:db"])
-    assert_response(resp, 2, resource_type="db")
-    resp = rtapi.get_resources(ResourceTypeFilters=["rds:snapshot"])
-    assert_response(resp, 2, resource_type="snapshot")
-    resp = rtapi.get_resources(TagFilters=[{"Key": "test", "Values": ["value-1"]}])
-    assert_response(resp, 2)
-
-
 @mock_lambda
 @mock_resourcegroupstaggingapi
 @mock_iam
@@ -717,3 +672,17 @@ def test_get_resources_lambda():
 
     resp = rtapi.get_resources(TagFilters=[{"Key": "Shape", "Values": ["rectangle"]}])
     assert_response(resp, [rectangle_arn])
+
+
+@mock_resourcegroupstaggingapi
+def test_tag_resources_for_unknown_service():
+    rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-west-2")
+    missing_resources = rtapi.tag_resources(
+        ResourceARNList=["arn:aws:service_x"], Tags={"key1": "k", "key2": "v"}
+    )["FailedResourcesMap"]
+
+    assert "arn:aws:service_x" in missing_resources
+    assert (
+        missing_resources["arn:aws:service_x"]["ErrorCode"]
+        == "InternalServiceException"
+    )