From f05d56afc5a99e28b1cc1fccc25c0b46ddc3167b Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Tue, 23 Aug 2022 19:47:44 +0000 Subject: [PATCH] Feature: Signer service (#5389) --- IMPLEMENTATION_COVERAGE.md | 24 ++- docs/docs/services/signer.rst | 55 +++++ moto/__init__.py | 1 + moto/backend_index.py | 1 + moto/signer/__init__.py | 5 + moto/signer/exceptions.py | 1 + moto/signer/models.py | 192 ++++++++++++++++++ moto/signer/responses.py | 43 ++++ moto/signer/urls.py | 15 ++ .../terraform-tests.success.txt | 7 + tests/test_signer/__init__.py | 0 tests/test_signer/test_signing_platforms.py | 21 ++ tests/test_signer/test_signing_profiles.py | 70 +++++++ 13 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 docs/docs/services/signer.rst create mode 100644 moto/signer/__init__.py create mode 100644 moto/signer/exceptions.py create mode 100644 moto/signer/models.py create mode 100644 moto/signer/responses.py create mode 100644 moto/signer/urls.py create mode 100644 tests/test_signer/__init__.py create mode 100644 tests/test_signer/test_signing_platforms.py create mode 100644 tests/test_signer/test_signing_profiles.py diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index dec6250a6..60e9bb278 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -5683,6 +5683,29 @@ - [X] verify_email_identity +## signer +
+23% implemented + +- [ ] add_profile_permission +- [X] cancel_signing_profile +- [ ] describe_signing_job +- [ ] get_signing_platform +- [X] get_signing_profile +- [ ] list_profile_permissions +- [ ] list_signing_jobs +- [X] list_signing_platforms +- [ ] list_signing_profiles +- [ ] list_tags_for_resource +- [X] put_signing_profile +- [ ] remove_profile_permission +- [ ] revoke_signature +- [ ] revoke_signing_profile +- [ ] start_signing_job +- [ ] tag_resource +- [ ] untag_resource +
+ ## sns
55% implemented @@ -6370,7 +6393,6 @@ - servicecatalog-appregistry - sesv2 - shield -- signer - sms - sms-voice - snow-device-management diff --git a/docs/docs/services/signer.rst b/docs/docs/services/signer.rst new file mode 100644 index 000000000..fffac2fae --- /dev/null +++ b/docs/docs/services/signer.rst @@ -0,0 +1,55 @@ +.. _implementedservice_signer: + +.. |start-h3| raw:: html + +

+ +.. |end-h3| raw:: html + +

+ +====== +signer +====== + +.. autoclass:: moto.signer.models.SignerBackend + +|start-h3| Example usage |end-h3| + +.. sourcecode:: python + + @mock_signer + def test_signer_behaviour: + boto3.client("signer") + ... + + + +|start-h3| Implemented features for this service |end-h3| + +- [ ] add_profile_permission +- [X] cancel_signing_profile +- [ ] describe_signing_job +- [ ] get_signing_platform +- [X] get_signing_profile +- [ ] list_profile_permissions +- [ ] list_signing_jobs +- [X] list_signing_platforms + + Pagination is not yet implemented. The parameters category, partner, target are not yet implemented + + +- [ ] list_signing_profiles +- [ ] list_tags_for_resource +- [X] put_signing_profile + + The following parameters are not yet implemented: SigningMaterial, Overrides, SigningParamaters + + +- [ ] remove_profile_permission +- [ ] revoke_signature +- [ ] revoke_signing_profile +- [ ] start_signing_job +- [ ] tag_resource +- [ ] untag_resource + diff --git a/moto/__init__.py b/moto/__init__.py index c99b6f3b6..6b62b5efb 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -137,6 +137,7 @@ mock_sdb = lazy_load(".sdb", "mock_sdb") mock_secretsmanager = lazy_load(".secretsmanager", "mock_secretsmanager") mock_ses = lazy_load(".ses", "mock_ses") mock_servicediscovery = lazy_load(".servicediscovery", "mock_servicediscovery") +mock_signer = lazy_load(".signer", "mock_signer", boto3_name="signer") mock_sns = lazy_load(".sns", "mock_sns") mock_sqs = lazy_load(".sqs", "mock_sqs") mock_ssm = lazy_load(".ssm", "mock_ssm") diff --git a/moto/backend_index.py b/moto/backend_index.py index 975d24440..6b6c934e2 100644 --- a/moto/backend_index.py +++ b/moto/backend_index.py @@ -143,6 +143,7 @@ backend_url_patterns = [ ), ("ses", re.compile("https?://email\\.(.+)\\.amazonaws\\.com")), ("ses", re.compile("https?://ses\\.(.+)\\.amazonaws\\.com")), + ("signer", re.compile("https?://signer\\.(.+)\\.amazonaws\\.com")), ("sns", re.compile("https?://sns\\.(.+)\\.amazonaws\\.com")), ("sqs", re.compile("https?://(.*\\.)?(queue|sqs)\\.(.*\\.)?amazonaws\\.com")), ("ssm", re.compile("https?://ssm\\.(.+)\\.amazonaws\\.com")), diff --git a/moto/signer/__init__.py b/moto/signer/__init__.py new file mode 100644 index 000000000..2972ffddf --- /dev/null +++ b/moto/signer/__init__.py @@ -0,0 +1,5 @@ +"""signer module initialization; sets value for base decorator.""" +from .models import signer_backends +from ..core.models import base_decorator + +mock_signer = base_decorator(signer_backends) diff --git a/moto/signer/exceptions.py b/moto/signer/exceptions.py new file mode 100644 index 000000000..3d60ba7c8 --- /dev/null +++ b/moto/signer/exceptions.py @@ -0,0 +1 @@ +"""Exceptions raised by the signer service.""" diff --git a/moto/signer/models.py b/moto/signer/models.py new file mode 100644 index 000000000..85d974cb2 --- /dev/null +++ b/moto/signer/models.py @@ -0,0 +1,192 @@ +from moto.core import BaseBackend, BaseModel +from moto.core.utils import BackendDict, get_random_hex + + +class SigningProfile(BaseModel): + def __init__( + self, account_id, region, name, platform_id, signature_validity_period, tags + ): + self.name = name + self.platform_id = platform_id + self.signature_validity_period = signature_validity_period or { + "value": 135, + "type": "MONTHS", + } + self.tags = tags + + self.status = "Active" + self.arn = f"arn:aws:signer:{region}:{account_id}:/signing-profiles/{name}" + self.profile_version = get_random_hex(10) + self.profile_version_arn = f"{self.arn}/{self.profile_version}" + + def cancel(self): + self.status = "Canceled" + + def to_dict(self, full=True): + small = { + "arn": self.arn, + "profileVersion": self.profile_version, + "profileVersionArn": self.profile_version_arn, + } + if full: + small.update( + { + "status": self.status, + "profileName": self.name, + "platformId": self.platform_id, + "signatureValidityPeriod": self.signature_validity_period, + "signingMaterial": {}, + "platformDisplayName": next( + ( + p["displayName"] + for p in SignerBackend.platforms + if p["platformId"] == self.platform_id + ), + None, + ), + } + ) + if self.tags: + small.update({"tags": self.tags}) + return small + + +class SignerBackend(BaseBackend): + """Implementation of signer APIs.""" + + platforms = [ + { + "platformId": "AWSIoTDeviceManagement-SHA256-ECDSA", + "displayName": "AWS IoT Device Management SHA256-ECDSA ", + "partner": "AWSIoTDeviceManagement", + "target": "SHA256-ECDSA", + "category": "AWS", + "signingConfiguration": { + "encryptionAlgorithmOptions": { + "allowedValues": ["ECDSA"], + "defaultValue": "ECDSA", + }, + "hashAlgorithmOptions": { + "allowedValues": ["SHA256"], + "defaultValue": "SHA256", + }, + }, + "signingImageFormat": { + "supportedFormats": ["JSONDetached"], + "defaultFormat": "JSONDetached", + }, + "maxSizeInMB": 2048, + "revocationSupported": False, + }, + { + "platformId": "AWSLambda-SHA384-ECDSA", + "displayName": "AWS Lambda", + "partner": "AWSLambda", + "target": "SHA384-ECDSA", + "category": "AWS", + "signingConfiguration": { + "encryptionAlgorithmOptions": { + "allowedValues": ["ECDSA"], + "defaultValue": "ECDSA", + }, + "hashAlgorithmOptions": { + "allowedValues": ["SHA384"], + "defaultValue": "SHA384", + }, + }, + "signingImageFormat": { + "supportedFormats": ["JSONDetached"], + "defaultFormat": "JSONDetached", + }, + "maxSizeInMB": 250, + "revocationSupported": True, + }, + { + "platformId": "AmazonFreeRTOS-TI-CC3220SF", + "displayName": "Amazon FreeRTOS SHA1-RSA CC3220SF-Format", + "partner": "AmazonFreeRTOS", + "target": "SHA1-RSA-TISHA1", + "category": "AWS", + "signingConfiguration": { + "encryptionAlgorithmOptions": { + "allowedValues": ["RSA"], + "defaultValue": "RSA", + }, + "hashAlgorithmOptions": { + "allowedValues": ["SHA1"], + "defaultValue": "SHA1", + }, + }, + "signingImageFormat": { + "supportedFormats": ["JSONEmbedded"], + "defaultFormat": "JSONEmbedded", + }, + "maxSizeInMB": 16, + "revocationSupported": False, + }, + { + "platformId": "AmazonFreeRTOS-Default", + "displayName": "Amazon FreeRTOS SHA256-ECDSA", + "partner": "AmazonFreeRTOS", + "target": "SHA256-ECDSA", + "category": "AWS", + "signingConfiguration": { + "encryptionAlgorithmOptions": { + "allowedValues": ["ECDSA", "RSA"], + "defaultValue": "ECDSA", + }, + "hashAlgorithmOptions": { + "allowedValues": ["SHA256"], + "defaultValue": "SHA256", + }, + }, + "signingImageFormat": { + "supportedFormats": ["JSONEmbedded"], + "defaultFormat": "JSONEmbedded", + }, + "maxSizeInMB": 16, + "revocationSupported": False, + }, + ] + + def __init__(self, region_name, account_id): + super().__init__(region_name, account_id) + self.signing_profiles: [str, SigningProfile] = dict() + + def cancel_signing_profile(self, profile_name) -> None: + self.signing_profiles[profile_name].cancel() + + def get_signing_profile(self, profile_name) -> SigningProfile: + return self.signing_profiles[profile_name] + + def put_signing_profile( + self, + profile_name, + signature_validity_period, + platform_id, + tags, + ) -> SigningProfile: + """ + The following parameters are not yet implemented: SigningMaterial, Overrides, SigningParamaters + """ + profile = SigningProfile( + account_id=self.account_id, + region=self.region_name, + name=profile_name, + platform_id=platform_id, + signature_validity_period=signature_validity_period, + tags=tags, + ) + self.signing_profiles[profile_name] = profile + return profile + + def list_signing_platforms(self): + """ + Pagination is not yet implemented. The parameters category, partner, target are not yet implemented + """ + return SignerBackend.platforms + + +# Using the lambda-regions +# boto3.Session().get_available_regions("signer") still returns an empty list +signer_backends: [str, [str, SignerBackend]] = BackendDict(SignerBackend, "lambda") diff --git a/moto/signer/responses.py b/moto/signer/responses.py new file mode 100644 index 000000000..ad42aa587 --- /dev/null +++ b/moto/signer/responses.py @@ -0,0 +1,43 @@ +"""Handles incoming signer requests, invokes methods, returns responses.""" +import json + +from moto.core.responses import BaseResponse +from .models import signer_backends, SignerBackend + + +class signerResponse(BaseResponse): + def __init__(self): + super().__init__(service_name="signer") + + @property + def signer_backend(self) -> SignerBackend: + """Return backend instance specific for this region.""" + return signer_backends[self.current_account][self.region] + + def cancel_signing_profile(self): + profile_name = self.path.split("/")[-1] + self.signer_backend.cancel_signing_profile(profile_name=profile_name) + return "{}" + + def get_signing_profile(self): + profile_name = self.path.split("/")[-1] + profile = self.signer_backend.get_signing_profile(profile_name=profile_name) + return json.dumps(profile.to_dict()) + + def put_signing_profile(self): + params = json.loads(self.body) + profile_name = self.path.split("/")[-1] + signature_validity_period = params.get("signatureValidityPeriod") + platform_id = params.get("platformId") + tags = params.get("tags") + profile = self.signer_backend.put_signing_profile( + profile_name=profile_name, + signature_validity_period=signature_validity_period, + platform_id=platform_id, + tags=tags, + ) + return json.dumps(profile.to_dict(full=False)) + + def list_signing_platforms(self): + platforms = self.signer_backend.list_signing_platforms() + return json.dumps(dict(platforms=platforms)) diff --git a/moto/signer/urls.py b/moto/signer/urls.py new file mode 100644 index 000000000..4b67c84ed --- /dev/null +++ b/moto/signer/urls.py @@ -0,0 +1,15 @@ +"""signer base URL and path.""" +from .responses import signerResponse + +url_bases = [ + r"https?://signer\.(.+)\.amazonaws\.com", +] + + +response = signerResponse() + + +url_paths = { + "{0}/signing-profiles/(?P[^/]+)$": response.dispatch, + "{0}/signing-platforms$": response.dispatch, +} diff --git a/tests/terraformtests/terraform-tests.success.txt b/tests/terraformtests/terraform-tests.success.txt index 262ba0f30..3a577cd5d 100644 --- a/tests/terraformtests/terraform-tests.success.txt +++ b/tests/terraformtests/terraform-tests.success.txt @@ -207,6 +207,13 @@ servicediscovery: - TestAccServiceDiscoveryPrivateDNSNamespace - TestAccServiceDiscoveryPublicDNSNamespace - TestAccServiceDiscoveryService +signer: + - TestAccSignerSigningProfileDataSource_basic + - TestAccSignerSigningProfile_basic + - TestAccSignerSigningProfile_generateNameWithNamePrefix + - TestAccSignerSigningProfile_generateName + - TestAccSignerSigningProfile_tags + - TestAccSignerSigningProfile_signatureValidityPeriod sns: - TestAccSNSTopicPolicy - TestAccSNSTopicDataSource diff --git a/tests/test_signer/__init__.py b/tests/test_signer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_signer/test_signing_platforms.py b/tests/test_signer/test_signing_platforms.py new file mode 100644 index 000000000..1000be347 --- /dev/null +++ b/tests/test_signer/test_signing_platforms.py @@ -0,0 +1,21 @@ +"""Unit tests for signer-supported APIs.""" +import boto3 + +import sure # noqa # pylint: disable=unused-import +from moto import mock_signer + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_signer +def test_list_signing_platforms(): + client = boto3.client("signer", region_name="us-east-2") + resp = client.list_signing_platforms() + + resp.should.have.key("platforms").should.have.length_of(4) + + partners = [x["partner"] for x in resp["platforms"]] + set(partners).should.equal( + {"AmazonFreeRTOS", "AWSLambda", "AWSIoTDeviceManagement"} + ) diff --git a/tests/test_signer/test_signing_profiles.py b/tests/test_signer/test_signing_profiles.py new file mode 100644 index 000000000..e7d90ab06 --- /dev/null +++ b/tests/test_signer/test_signing_profiles.py @@ -0,0 +1,70 @@ +"""Unit tests for signer-supported APIs.""" +import boto3 + +import sure # noqa # pylint: disable=unused-import +from moto import mock_signer + +# See our Development Tips on writing tests for hints on how to write good tests: +# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html + + +@mock_signer +def test_put_signing_profile(): + client = boto3.client("signer", region_name="eu-west-1") + resp = client.put_signing_profile(profileName="prof1", platformId="pid") + + resp.should.have.key("arn") + resp.should.have.key("profileVersion") + resp.should.have.key("profileVersionArn") + + +@mock_signer +def test_get_signing_profile(): + client = boto3.client("signer", region_name="eu-west-1") + resp = client.put_signing_profile( + profileName="prof1", platformId="AWSLambda-SHA384-ECDSA" + ) + + resp = client.get_signing_profile(profileName="prof1") + + resp.should.have.key("arn") + resp.should.have.key("profileVersion") + resp.should.have.key("profileVersionArn") + resp.should.have.key("status").equals("Active") + resp.should.have.key("profileName").equals("prof1") + resp.should.have.key("platformId").equals("AWSLambda-SHA384-ECDSA") + resp.should.have.key("signatureValidityPeriod").equals( + {"type": "MONTHS", "value": 135} + ) + + +@mock_signer +def test_get_signing_profile__with_args(): + client = boto3.client("signer", region_name="eu-west-1") + resp = client.put_signing_profile( + profileName="prof1", + platformId="AWSLambda-SHA384-ECDSA", + signatureValidityPeriod={"type": "DAYS", "value": 10}, + tags={"k1": "v1", "k2": "v2"}, + ) + + resp = client.get_signing_profile(profileName="prof1") + + resp.should.have.key("signatureValidityPeriod").equals( + {"type": "DAYS", "value": 10} + ) + resp.should.have.key("tags").equals({"k1": "v1", "k2": "v2"}) + + +@mock_signer +def test_cancel_signing_profile(): + client = boto3.client("signer", region_name="eu-west-1") + resp = client.put_signing_profile( + profileName="prof1", platformId="AWSLambda-SHA384-ECDSA" + ) + + client.cancel_signing_profile(profileName="prof1") + + resp = client.get_signing_profile(profileName="prof1") + + resp.should.have.key("status").equals("Canceled")