Repository: getmoto/moto
Pull request #6702 (2023-08-20 00:46:50)

KMS: Add `RSASSA_PSS_SHA_256` private key
- Expanding `generate_private_key` method to be able to return multiple private key algorithm by using abstract class.
- Add `RSASSA_PSS_SHA_256` private key.
- We can use the following unittest for this changes.
https://github.com/getmoto/moto/blob/5e1b166f2521eacd737f2ccb241fbc0b2e8bb931/tests/test_kms/test_kms_boto3.py#L1166-L1194

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/6702?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
> Merging [#6702](https://app.codecov.io/gh/getmoto/moto/pull/6702?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (2b40c7d) into [master](https://app.codecov.io/gh/getmoto/moto/commit/5e1b166f2521eacd737f2ccb241fbc0b2e8bb931?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) (5e1b166) will **decrease** coverage by `0.01%`.
> The diff coverage is `89.79%`.

```diff
@@            Coverage Diff             @@
##           master    #6702      +/-   ##
==========================================
- Coverage   96.19%   96.18%   -0.01%     
==========================================
  Files         806      806              
  Lines       79106    79139      +33     
==========================================
+ Hits        76096    76121      +25     
- Misses       3010     3018       +8     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/6702/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/6702/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `36.87% <46.93%> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/6702/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `96.13% <89.79%> (-0.01%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files Changed](https://app.codecov.io/gh/getmoto/moto/pull/6702?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [moto/kms/utils.py](https://app.codecov.io/gh/getmoto/moto/pull/6702?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9rbXMvdXRpbHMucHk=) | `96.96% <88.88%> (-3.04%)` | :arrow_down: |
| [moto/kms/models.py](https://app.codecov.io/gh/getmoto/moto/pull/6702?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9rbXMvbW9kZWxzLnB5) | `93.91% <100.00%> (-0.18%)` | :arrow_down: |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/getmoto/moto/pull/6702/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto)



Patch:
diff --git a/moto/iot/responses.py b/moto/iot/responses.py
index 8f6f52521..2daec7068 100644
--- a/moto/iot/responses.py
+++ b/moto/iot/responses.py
@@ -581,14 +581,14 @@ class IoTResponse(BaseResponse):
         return json.dumps(dict(principals=principals))
 
     def describe_thing_group(self) -> str:
-        thing_group_name = self._get_param("thingGroupName")
+        thing_group_name = unquote(self.path.split("/thing-groups/")[-1])
         thing_group = self.iot_backend.describe_thing_group(
             thing_group_name=thing_group_name
         )
         return json.dumps(thing_group.to_dict())
 
     def create_thing_group(self) -> str:
-        thing_group_name = self._get_param("thingGroupName")
+        thing_group_name = unquote(self.path.split("/thing-groups/")[-1])
         parent_group_name = self._get_param("parentGroupName")
         thing_group_properties = self._get_param("thingGroupProperties")
         (
@@ -609,7 +609,7 @@ class IoTResponse(BaseResponse):
         )
 
     def delete_thing_group(self) -> str:
-        thing_group_name = self._get_param("thingGroupName")
+        thing_group_name = unquote(self.path.split("/thing-groups/")[-1])
         self.iot_backend.delete_thing_group(thing_group_name=thing_group_name)
         return json.dumps(dict())
 
@@ -632,7 +632,7 @@ class IoTResponse(BaseResponse):
         return json.dumps(dict(thingGroups=rets, nextToken=next_token))
 
     def update_thing_group(self) -> str:
-        thing_group_name = self._get_param("thingGroupName")
+        thing_group_name = unquote(self.path.split("/thing-groups/")[-1])
         thing_group_properties = self._get_param("thingGroupProperties")
         expected_version = self._get_param("expectedVersion")
         version = self.iot_backend.update_thing_group(
diff --git a/moto/kms/models.py b/moto/kms/models.py
index 5d0c202fb..461ce5c15 100644
--- a/moto/kms/models.py
+++ b/moto/kms/models.py
@@ -3,9 +3,6 @@ import os
 from collections import defaultdict
 from copy import copy
 from datetime import datetime, timedelta
-from cryptography.exceptions import InvalidSignature
-from cryptography.hazmat.primitives import hashes, serialization
-from cryptography.hazmat.primitives.asymmetric import padding
 from typing import Any, Dict, List, Tuple, Optional, Iterable, Set
 
 from moto.core import BaseBackend, BackendDict, BaseModel, CloudFormationModel
@@ -82,10 +79,10 @@ class Key(CloudFormationModel):
         self.key_rotation_status = False
         self.deletion_date: Optional[datetime] = None
         self.key_material = generate_master_key()
-        self.private_key = generate_private_key()
         self.origin = "AWS_KMS"
         self.key_manager = "CUSTOMER"
         self.key_spec = key_spec or "SYMMETRIC_DEFAULT"
+        self.private_key = generate_private_key(self.key_spec)
         self.arn = f"arn:aws:kms:{region}:{account_id}:key/{self.id}"
 
         self.grants: Dict[str, Grant] = dict()
@@ -641,14 +638,7 @@ class KmsBackend(BaseBackend):
         self.__ensure_valid_sign_and_verify_key(key)
         self.__ensure_valid_signing_algorithm(key, signing_algorithm)
 
-        # TODO: support more than one hardcoded algorithm based on KeySpec
-        signature = key.private_key.sign(
-            message,
-            padding.PSS(
-                mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
-            ),
-            hashes.SHA256(),
-        )
+        signature = key.private_key.sign(message, signing_algorithm)
 
         return key.arn, signature, signing_algorithm
 
@@ -678,31 +668,15 @@ class KmsBackend(BaseBackend):
                 )
             )
 
-        public_key = key.private_key.public_key()
-
-        try:
-            # TODO: support more than one hardcoded algorithm based on KeySpec
-            public_key.verify(
-                signature,
-                message,
-                padding.PSS(
-                    mgf=padding.MGF1(hashes.SHA256()),
-                    salt_length=padding.PSS.MAX_LENGTH,
-                ),
-                hashes.SHA256(),
-            )
-            return key.arn, True, signing_algorithm
-        except InvalidSignature:
-            return key.arn, False, signing_algorithm
+        return (
+            key.arn,
+            key.private_key.verify(message, signature, signing_algorithm),
+            signing_algorithm,
+        )
 
     def get_public_key(self, key_id: str) -> Tuple[Key, bytes]:
         key = self.describe_key(key_id)
-        public_key = key.private_key.public_key().public_bytes(
-            encoding=serialization.Encoding.DER,
-            format=serialization.PublicFormat.SubjectPublicKeyInfo,
-        )
-
-        return key, public_key
+        return key, key.private_key.public_key()
 
 
 kms_backends = BackendDict(KmsBackend, "kms")
diff --git a/moto/kms/utils.py b/moto/kms/utils.py
index 4b4323acf..754eb7b2b 100644
--- a/moto/kms/utils.py
+++ b/moto/kms/utils.py
@@ -1,3 +1,4 @@
+from abc import abstractmethod, ABCMeta
 from collections import namedtuple
 from typing import Any, Dict, Tuple, List
 from enum import Enum
@@ -6,9 +7,12 @@ import os
 import struct
 from moto.moto_api._internal import mock_random
 
+from cryptography.exceptions import InvalidSignature
 from cryptography.hazmat.backends import default_backend
 from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
-from cryptography.hazmat.primitives.asymmetric import rsa
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa, padding
+
 
 from .exceptions import (
     InvalidCiphertextException,
@@ -134,16 +138,92 @@ def generate_master_key() -> bytes:
     return generate_data_key(MASTER_KEY_LEN)
 
 
-def generate_private_key() -> rsa.RSAPrivateKey:
-    """Generate a private key to be used on asymmetric sign/verify.
+class AbstractPrivateKey(metaclass=ABCMeta):
+    @abstractmethod
+    def sign(self, message: bytes, signing_algorithm: str) -> bytes:
+        raise NotImplementedError
 
-    NOTE: KeySpec is not taken into consideration and the key is always RSA_2048
-    this could be improved to support multiple key types
-    """
-    return rsa.generate_private_key(
-        public_exponent=65537,
-        key_size=2048,
-    )
+    @abstractmethod
+    def verify(self, message: bytes, signature: bytes, signing_algorithm: str) -> bool:
+        raise NotImplementedError
+
+    @abstractmethod
+    def public_key(self) -> bytes:
+        raise NotImplementedError
+
+
+def validate_signing_algorithm(
+    target_algorithm: str, valid_algorithms: List[str]
+) -> None:
+    if target_algorithm not in valid_algorithms:
+        raise ValidationException(
+            (
+                "1 validation error detected: Value at 'signing_algorithm' failed"
+                "to satisfy constraint: Member must satisfy enum value set: {valid_signing_algorithms}"
+            ).format(valid_signing_algorithms=valid_algorithms)
+        )
+
+
+class RSAPrivateKey(AbstractPrivateKey):
+    def __init__(self, key_size: int):
+        self.key_size = key_size
+        self.private_key = rsa.generate_private_key(
+            public_exponent=65537, key_size=self.key_size
+        )
+
+    def sign(self, message: bytes, signing_algorithm: str) -> bytes:
+        validate_signing_algorithm(
+            signing_algorithm, SigningAlgorithm.rsa_signing_algorithms()
+        )
+
+        if signing_algorithm == SigningAlgorithm.RSASSA_PSS_SHA_256:
+            pad = padding.PSS(
+                mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
+            )
+            algorithm = hashes.SHA256()
+        else:
+            pad = padding.PSS(
+                mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
+            )
+            algorithm = hashes.SHA256()
+        return self.private_key.sign(message, pad, algorithm)
+
+    def verify(self, message: bytes, signature: bytes, signing_algorithm: str) -> bool:
+        validate_signing_algorithm(
+            signing_algorithm, SigningAlgorithm.rsa_signing_algorithms()
+        )
+
+        if signing_algorithm == SigningAlgorithm.RSASSA_PSS_SHA_256:
+            pad = padding.PSS(
+                mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
+            )
+            algorithm = hashes.SHA256()
+        else:
+            pad = padding.PSS(
+                mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH
+            )
+            algorithm = hashes.SHA256()
+
+        public_key = self.private_key.public_key()
+        try:
+            public_key.verify(signature, message, pad, algorithm)
+            return True
+        except InvalidSignature:
+            return False
+
+    def public_key(self) -> bytes:
+        return self.private_key.public_key().public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.SubjectPublicKeyInfo,
+        )
+
+
+def generate_private_key(key_spec: str) -> AbstractPrivateKey:
+    """Generate a private key to be used on asymmetric sign/verify."""
+    if key_spec == KeySpec.RSA_2048:
+        return RSAPrivateKey(key_size=2048)
+    else:
+        return RSAPrivateKey(key_size=2048)
 
 
 def _serialize_ciphertext_blob(ciphertext: Ciphertext) -> bytes:


Test patch:
diff --git a/tests/test_iot/test_iot_thing_groups.py b/tests/test_iot/test_iot_thing_groups.py
index d69a5b218..6c343ab63 100644
--- a/tests/test_iot/test_iot_thing_groups.py
+++ b/tests/test_iot/test_iot_thing_groups.py
@@ -38,8 +38,8 @@ def generate_thing_group_tree(iot_client, tree_dict, _parent=None):
 
 
 class TestListThingGroup:
-    group_name_1a = "my-group-name-1a"
-    group_name_1b = "my-group-name-1b"
+    group_name_1a = "my-group:name-1a"
+    group_name_1b = "my-group:name-1b"
     group_name_2a = "my-group-name-2a"
     group_name_2b = "my-group-name-2b"
     group_name_3a = "my-group-name-3a"
@@ -112,7 +112,7 @@ class TestListThingGroup:
         client = boto3.client("iot", region_name="ap-northeast-1")
         generate_thing_group_tree(client, self.tree_dict)
         # test
-        resp = client.list_thing_groups(namePrefixFilter="my-group-name-1")
+        resp = client.list_thing_groups(namePrefixFilter="my-group:name-1")
         assert "thingGroups" in resp
         assert len(resp["thingGroups"]) == 2
         resp = client.list_thing_groups(namePrefixFilter="my-group-name-3")
@@ -129,7 +129,7 @@ class TestListThingGroup:
         generate_thing_group_tree(client, self.tree_dict)
         # test
         resp = client.list_thing_groups(
-            namePrefixFilter="my-group-name-1", recursive=False
+            namePrefixFilter="my-group:name-1", recursive=False
         )
         assert "thingGroups" in resp
         assert len(resp["thingGroups"]) == 2
@@ -166,7 +166,7 @@ class TestListThingGroup:
 @mock_iot
 def test_delete_thing_group():
     client = boto3.client("iot", region_name="ap-northeast-1")
-    group_name_1a = "my-group-name-1a"
+    group_name_1a = "my-group:name-1a"
     group_name_2a = "my-group-name-2a"
     tree_dict = {group_name_1a: {group_name_2a: {}}}
     generate_thing_group_tree(client, tree_dict)
@@ -199,8 +199,8 @@ def test_delete_thing_group():
 @mock_iot
 def test_describe_thing_group_metadata_hierarchy():
     client = boto3.client("iot", region_name="ap-northeast-1")
-    group_name_1a = "my-group-name-1a"
-    group_name_1b = "my-group-name-1b"
+    group_name_1a = "my-group:name-1a"
+    group_name_1b = "my-group:name-1b"
     group_name_2a = "my-group-name-2a"
     group_name_2b = "my-group-name-2b"
     group_name_3a = "my-group-name-3a"
@@ -313,7 +313,7 @@ def test_describe_thing_group_metadata_hierarchy():
 @mock_iot
 def test_thing_groups():
     client = boto3.client("iot", region_name="ap-northeast-1")
-    group_name = "my-group-name"
+    group_name = "my-group:name"
 
     # thing group
     thing_group = client.create_thing_group(thingGroupName=group_name)
diff --git a/tests/test_kms/test_kms_boto3.py b/tests/test_kms/test_kms_boto3.py
index 052bed9e4..f11816cc1 100644
--- a/tests/test_kms/test_kms_boto3.py
+++ b/tests/test_kms/test_kms_boto3.py
@@ -1163,7 +1163,7 @@ def test_sign_and_verify_ignoring_grant_tokens():
 
 
 @mock_kms
-def test_sign_and_verify_digest_message_type_256():
+def test_sign_and_verify_digest_message_type_RSASSA_PSS_SHA_256():
     client = boto3.client("kms", region_name="us-west-2")
 
     key = client.create_key(