From c0569f043384c694715c38cf8364c69ea907e850 Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 11:01:08 +0300 Subject: [PATCH 01/10] Implement Onboarding --- constants/__init__.py | 0 constants/keys.py | 0 constants/media_types.py | 5 ++ environments/enums.py | 0 environments/environmental_services.py | 2 +- onboarding/__init__.py | 0 onboarding/enums.py | 11 +++ onboarding/exceptions.py | 15 ++++ onboarding/headers.py | 48 ++++++++++++ onboarding/onboarding.py | 70 +++++++++++++++++ onboarding/parameters.py | 95 +++++++++++++++++++++++ onboarding/request.py | 53 +++++++++++++ onboarding/request_body.py | 101 +++++++++++++++++++++++++ onboarding/response.py | 29 +++++++ onboarding/signature.py | 29 +++++++ revoking/__init__.py | 0 revoking/headers.py | 23 ++++++ revoking/parameters.py | 34 +++++++++ revoking/request.py | 36 +++++++++ revoking/request_body.py | 30 ++++++++ revoking/response.py | 7 ++ revoking/revoking.py | 30 ++++++++ 22 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 constants/__init__.py create mode 100644 constants/keys.py create mode 100644 constants/media_types.py create mode 100644 environments/enums.py create mode 100644 onboarding/__init__.py create mode 100644 onboarding/enums.py create mode 100644 onboarding/exceptions.py create mode 100644 onboarding/headers.py create mode 100644 onboarding/onboarding.py create mode 100644 onboarding/parameters.py create mode 100644 onboarding/request.py create mode 100644 onboarding/request_body.py create mode 100644 onboarding/response.py create mode 100644 onboarding/signature.py create mode 100644 revoking/__init__.py create mode 100644 revoking/headers.py create mode 100644 revoking/parameters.py create mode 100644 revoking/request.py create mode 100644 revoking/request_body.py create mode 100644 revoking/response.py create mode 100644 revoking/revoking.py diff --git a/constants/__init__.py b/constants/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/constants/keys.py b/constants/keys.py new file mode 100644 index 00000000..e69de29b diff --git a/constants/media_types.py b/constants/media_types.py new file mode 100644 index 00000000..a77638f6 --- /dev/null +++ b/constants/media_types.py @@ -0,0 +1,5 @@ +from auth.enums import BaseEnum + + +class ContentTypes(BaseEnum): + APPLICATION_JSON = "application/json" diff --git a/environments/enums.py b/environments/enums.py new file mode 100644 index 00000000..e69de29b diff --git a/environments/environmental_services.py b/environments/environmental_services.py index 5d9da90f..f980cd2f 100644 --- a/environments/environmental_services.py +++ b/environments/environmental_services.py @@ -12,4 +12,4 @@ def _set_env(self, env) -> None: elif env == "Production": self._environment = ProductionEnvironment() else: - raise InvalidEnvironmentSetup(env=env) \ No newline at end of file + raise InvalidEnvironmentSetup(env=env) diff --git a/onboarding/__init__.py b/onboarding/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/onboarding/enums.py b/onboarding/enums.py new file mode 100644 index 00000000..cc7dbd31 --- /dev/null +++ b/onboarding/enums.py @@ -0,0 +1,11 @@ +from auth.enums import BaseEnum + + +class CertificateTypes(BaseEnum): + PEM = "PEM" + P12 = "P12" + + +class GateWays(BaseEnum): + MQTT = "2" + REST = "3" diff --git a/onboarding/exceptions.py b/onboarding/exceptions.py new file mode 100644 index 00000000..96686763 --- /dev/null +++ b/onboarding/exceptions.py @@ -0,0 +1,15 @@ +class AgriRouuterBaseException(Exception): + _message = ... + + def __init__(self, message=None): + if not message: + message = self._message + self.message = message + + +class WrongCertificationType(AgriRouuterBaseException): + _message = "Wrong Certification type. Use onboarding.enums.CertificationTypes values instead." + + +class WrongGateWay(AgriRouuterBaseException): + _message = "Wrong Gate Way Id. Use onboarding.enums.GateWays values instead." diff --git a/onboarding/headers.py b/onboarding/headers.py new file mode 100644 index 00000000..32b17a56 --- /dev/null +++ b/onboarding/headers.py @@ -0,0 +1,48 @@ +from abc import ABC, abstractmethod + +from constants.media_types import ContentTypes + + +class BaseOnboardingHeader(ABC): + @abstractmethod + def __init__(self, *args, **kwargs): + self._set_params(*args, **kwargs) + + @abstractmethod + def get_header(self) -> dict: + ... + + @abstractmethod + def _set_params(self, *args, **kwargs): + ... + + +class SoftwareOnboardingHeader(BaseOnboardingHeader): + def __init__(self, + reg_code, + application_id, + signature=None, + content_type=ContentTypes.APPLICATION_JSON.value + ): + + self._set_params(reg_code, application_id, signature, content_type) + + def get_header(self) -> dict: + return self.params + + def sign(self, signature): + self.params["X-Agrirouter-Signature"] = signature + + def _set_params(self, reg_code: str, application_id: str, signature: str, content_type: str): + header = dict() + header["Authorization"] = f"Bearer {reg_code}" + header["Content-Type"] = content_type + header["X-Agrirouter-ApplicationId"] = application_id + if signature: + header["X-Agrirouter-Signature"] = signature + + self.params = header + + +class CUOnboardingHeader(BaseOnboardingHeader): + pass diff --git a/onboarding/onboarding.py b/onboarding/onboarding.py new file mode 100644 index 00000000..28071c54 --- /dev/null +++ b/onboarding/onboarding.py @@ -0,0 +1,70 @@ +import requests + +from environments.environmental_services import EnvironmentalService +from onboarding.headers import SoftwareOnboardingHeader, CUOnboardingHeader +from onboarding.parameters import SoftwareOnboardingParameter, BaseOnboardingParameter, RevokingParameter, \ + CUOnboardingParameter +from onboarding.request import SoftwareOnboardingRequest, BaseOnboardingRequest, CUOnboardingRequest +from onboarding.request_body import SoftwareOnboardingBody, CUOnboardingBody +from onboarding.response import SoftwareVerifyOnboardingResponse, SoftwareOnboardingResponse, RevokingResponse, \ + CUOnboardingResponse + + +class SoftwareOnboarding(EnvironmentalService): + + def _create_request(self, params: BaseOnboardingParameter, url: str) -> SoftwareOnboardingRequest: + body_params = params.get_body_params() + request_body = SoftwareOnboardingBody(**body_params) + + header_params = params.get_header_params() + header_params["request_body"] = request_body.json(new_lines=False) + request_header = SoftwareOnboardingHeader(**header_params) + + return SoftwareOnboardingRequest(header=request_header, body=request_body, url=url) + + def _perform_request(self, params: BaseOnboardingParameter, url: str) -> requests.Response: + request = self._create_request(params, url) + return requests.post( + url=request.get_url(), + data=request.get_data(), + headers=request.get_header() + ) + + def verify(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResponse: + url = self._environment.get_verify_onboard_request_url() + http_response = self._perform_request(params=params, url=url) + + return SoftwareOnboardingResponse(http_response) + + def onboard(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResponse: + url = self._environment.get_secured_onboard_url() + http_response = self._perform_request(params=params, url=url) + + return SoftwareOnboardingResponse(http_response) + + +class CUOnboarding(EnvironmentalService): + + def _create_request(self, params: CUOnboardingParameter, url: str) -> CUOnboardingRequest: + body_params = params.get_body_params() + request_body = CUOnboardingBody(**body_params) + + header_params = params.get_header_params() + header_params["request_body"] = request_body.json(new_lines=False) + request_header = CUOnboardingHeader(**header_params) + + return CUOnboardingRequest(header=request_header, body=request_body, url=url) + + def _perform_request(self, params: CUOnboardingParameter, url: str) -> requests.Response: + request = self._create_request(params, url) + return requests.post( + url=request.get_url(), + data=request.get_data(), + headers=request.get_header() + ) + + def onboard(self, params: CUOnboardingParameter) -> CUOnboardingResponse: + url = self._environment.get_onboard_url() + http_response = self._perform_request(params=params, url=url) + + return CUOnboardingResponse(http_response) diff --git a/onboarding/parameters.py b/onboarding/parameters.py new file mode 100644 index 00000000..b8a4f879 --- /dev/null +++ b/onboarding/parameters.py @@ -0,0 +1,95 @@ +from abc import ABC, abstractmethod + +from constants.media_types import ContentTypes +from onboarding.enums import CertificateTypes + + +class BaseOnboardingParameter(ABC): + @abstractmethod + def __init__(self, *args, **kwargs): + ... + + @abstractmethod + def get_header_params(self, *args, **kwargs): + ... + + @abstractmethod + def get_body_params(self, *args, **kwargs): + ... + + +class SoftwareOnboardingParameter(BaseOnboardingParameter): + def __init__(self, + id_, + application_id, + certification_version_id, + gateway_id, + utc_timestamp, + time_zone, + reg_code, + content_type=ContentTypes.APPLICATION_JSON.value, + certificate_type=CertificateTypes.P12.value, + ): + + self.id_ = id_ + self.application_id = application_id + self.content_type = content_type + self.certification_version_id = certification_version_id + self.gateway_id = gateway_id + self.certificate_type = certificate_type + self.utc_timestamp = utc_timestamp + self.time_zone = time_zone + self.reg_code = reg_code + + def get_header_params(self): + return { + "content_type": self.content_type, + "reg_code": self.reg_code, + "application_id": self.application_id, + } + + def get_body_params(self): + return { + "id": self.id_, + "application_id": self.application_id, + "certification_version_id": self.certification_version_id, + "gateway_id": self.gateway_id, + "certificate_type": self.certificate_type, + "utc_timestamp": self.utc_timestamp, + "time_zone": self.time_zone, + } + + +class CUOnboardingParameter(BaseOnboardingParameter): + def __init__(self, + id_, + application_id, + certification_version_id, + gateway_id, + reg_code, + content_type=ContentTypes.APPLICATION_JSON.value, + certificate_type=CertificateTypes.P12.value, + ): + + self.id_ = id_ + self.application_id = application_id + self.content_type = content_type + self.certification_version_id = certification_version_id + self.gateway_id = gateway_id + self.certificate_type = certificate_type + self.reg_code = reg_code + + def get_header_params(self): + return { + "content_type": self.content_type, + "reg_code": self.reg_code, + } + + def get_body_params(self): + return { + "id": self.id_, + "application_id": self.application_id, + "certification_version_id": self.certification_version_id, + "gateway_id": self.gateway_id, + "certificate_type": self.certificate_type, + } diff --git a/onboarding/request.py b/onboarding/request.py new file mode 100644 index 00000000..5dda15b5 --- /dev/null +++ b/onboarding/request.py @@ -0,0 +1,53 @@ +from onboarding.headers import SoftwareOnboardingHeader, BaseOnboardingHeader +from onboarding.request_body import SoftwareOnboardingBody, BaseOnboardingBody + + +class BaseOnboardingRequest: + def __init__(self, header: BaseOnboardingHeader, body: BaseOnboardingBody, url: str): + self.header = header + self.body = body + self.url = url + + def get_url(self): + return self.url + + def get_data(self): + return self.body.get_parameters() + + def get_header(self): + return self.header.get_header() + + def sign(self): + """ + TODO: add here create_signature + :return: + """ + signature = ... # create signature + self.header.sign(signature) + pass + + @property + def is_signed(self): + header_has_signature = self.get_header().get("X-Agrirouter-Signature", None) + if header_has_signature: + return True + return False + + @property + def is_valid(self): + if not self.is_signed: + return False + + +class SoftwareOnboardingRequest(BaseOnboardingRequest): + """ + Request must be used to onboard Farming Software or Telemetry Platform + """ + pass + + +class CUOnboardingRequest(BaseOnboardingRequest): + """ + Request must be used to onboard CUs + """ + pass diff --git a/onboarding/request_body.py b/onboarding/request_body.py new file mode 100644 index 00000000..b4f922b6 --- /dev/null +++ b/onboarding/request_body.py @@ -0,0 +1,101 @@ +import json +from abc import ABC, abstractmethod + +from onboarding.enums import CertificateTypes, GateWays +from onboarding.exceptions import WrongCertificationType, WrongGateWay + + +class BaseOnboardingBody(ABC): + @abstractmethod + def __init__(self, *args, **kwargs): + ... + + @abstractmethod + def get_parameters(self, *args, **kwargs) -> dict: + ... + + @abstractmethod + def _set_params(self, *args, **kwargs): + ... + + +class SoftwareOnboardingBody(BaseOnboardingBody): + def __init__(self, + id_, + application_id, + certification_version_id, + gateway_id, + certificate_type, + utc_timestamp, + time_zone + ): + + self._validate_certificate_type(certificate_type) + self._validate_gateway_id(gateway_id) + + self._set_params( + id_, + application_id, + certification_version_id, + gateway_id, + certificate_type, + utc_timestamp, + time_zone + ) + + def get_parameters(self) -> dict: + return self.params + + def _set_params(self, + id_, + application_id, + certification_version_id, + gateway_id, + certificate_type, + utc_timestamp, + time_zone + ): + + self.params = { + "id": id_, + "applicationId": application_id, + "certificationVersionId": certification_version_id, + "gatewayId": gateway_id, + "certificateType": certificate_type, + "UTCTimestamp": utc_timestamp, + "timeZone": time_zone, + } + + def json(self, new_lines: bool = True) -> str: + result = json.dumps(self.get_parameters(), indent="") + if not new_lines: + return result.replace("\n", "") + return result + + @staticmethod + def _validate_certificate_type(certificate_type: str) -> None: + if certificate_type not in CertificateTypes.values_list(): + raise WrongCertificationType + + @staticmethod + def _validate_gateway_id(gateway_id: str) -> None: + if gateway_id not in GateWays.values_list(): + raise WrongGateWay + + +class CUOnboardingBody(BaseOnboardingBody): + + def __init__(self, *args, **kwargs): + ... + + def get_parameters(self, *args, **kwargs) -> dict: + ... + + def _set_params(self, *args, **kwargs): + ... + + def json(self, new_lines: bool = True) -> str: + result = json.dumps(self.get_parameters(), indent="") + if not new_lines: + return result.replace("\n", "") + return result diff --git a/onboarding/response.py b/onboarding/response.py new file mode 100644 index 00000000..e1d1d420 --- /dev/null +++ b/onboarding/response.py @@ -0,0 +1,29 @@ +from abc import ABC, abstractmethod + + +class BaseOnboardingResonse(ABC): + @abstractmethod + def __init__(self, http_response): + self.response = http_response + + +class SoftwareVerifyOnboardingResponse(BaseOnboardingResonse): + """ + Response from verify request used for Farming Software or Telemetry Platform before onboarding + """ + pass + + +class SoftwareOnboardingResponse(BaseOnboardingResonse): + """ + Response from onboarding request used for Farming Software or Telemetry Platform + """ + pass + + +class CUOnboardingResponse(BaseOnboardingResonse): + """ + Response from onboarding request used for CUs + """ + pass + diff --git a/onboarding/signature.py b/onboarding/signature.py new file mode 100644 index 00000000..fa68befc --- /dev/null +++ b/onboarding/signature.py @@ -0,0 +1,29 @@ +from cryptography.hazmat.primitives.serialization import load_pem_public_key, load_pem_private_key +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding + +from pprint import pprint + +SIGNATURE_ALGORITHM = "SHA256withRSA" + + +def create_signature(request_body: str, private_key: str) -> bytes: + private_key_bytes = bytearray(private_key.encode('utf-8')) + private_key_data = load_pem_private_key(private_key_bytes, None) + signature = private_key_data.sign( + request_body.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA256() + ) + return signature + + +def verify_signature(request_body: str, signature: bytearray, public_key: str) -> None: + public_key_bytes = bytearray(public_key.encode('utf-8')) + public_key_data = load_pem_public_key(public_key_bytes) + public_key_data.verify( + signature, + request_body.encode('utf-8'), + padding.PKCS1v15(), + hashes.SHA256() + ) diff --git a/revoking/__init__.py b/revoking/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/revoking/headers.py b/revoking/headers.py new file mode 100644 index 00000000..c4d1b7a8 --- /dev/null +++ b/revoking/headers.py @@ -0,0 +1,23 @@ +from constants.media_types import ContentTypes + + +class RevokingHeader: + def __init__(self, + application_id, + signature=None, + content_type=ContentTypes.APPLICATION_JSON.value + ): + + self._set_params(application_id, signature, content_type) + + def get_header(self) -> dict: + return self.params + + def _set_params(self, application_id: str, signature: str, content_type: str): + header = dict() + header["Content-Type"] = content_type + header["X-Agrirouter-ApplicationId"] = application_id + if signature: + header["X-Agrirouter-Signature"] = signature + + self.params = header \ No newline at end of file diff --git a/revoking/parameters.py b/revoking/parameters.py new file mode 100644 index 00000000..ef65d556 --- /dev/null +++ b/revoking/parameters.py @@ -0,0 +1,34 @@ +from constants.media_types import ContentTypes + + +class RevokingParameter: + + def __init__(self, + application_id, + account_id, + endpoint_ids, + utc_timestamp, + timestamp, + content_type=ContentTypes.APPLICATION_JSON.value + ): + + self.application_id = application_id + self.content_type = content_type + self.account_id = account_id + self.endpoint_ids = endpoint_ids + self.utc_timestamp = utc_timestamp + self.timestamp = timestamp + + def get_header_params(self): + return { + "application_id": self.application_id, + "content_type": self.content_type, + } + + def get_body_params(self): + return { + "account_id": self.account_id, + "endpoint_ids": self.endpoint_ids, + "utc_timestamp": self.utc_timestamp, + "timestamp": self.timestamp, + } diff --git a/revoking/request.py b/revoking/request.py new file mode 100644 index 00000000..aa7620cc --- /dev/null +++ b/revoking/request.py @@ -0,0 +1,36 @@ +class BaseOnboardingRequest: + def __init__(self, header: RevokingHeader, body: RevokingBody, url: str): + self.header = header + self.body = body + self.url = url + + def get_url(self): + return self.url + + def get_data(self): + return self.body.get_parameters() + + def get_header(self): + return self.header.get_header() + + def sign(self): + """ + TODO: add here create_signature + :return: + """ + signature = ... # create signature + self.header.sign(signature) + + @property + def is_signed(self) -> bool: + header_has_signature = self.get_header().get("X-Agrirouter-Signature", None) + if header_has_signature: + return True + return False + + @property + def is_valid(self) -> bool: + if not self.is_signed: + return False + signature = self.get_header().get("X-Agrirouter-Signature") + # return validate_signature(signature) diff --git a/revoking/request_body.py b/revoking/request_body.py new file mode 100644 index 00000000..e23c2750 --- /dev/null +++ b/revoking/request_body.py @@ -0,0 +1,30 @@ +class RevokingBody: + def __init__(self, + account_id, + endpoint_ids, + utc_timestamp, + time_zone): + + self._set_params( + account_id, + endpoint_ids, + utc_timestamp, + time_zone + ) + + def get_parameters(self) -> dict: + return self.params + + def _set_params(self, + account_id, + endpoint_ids, + utc_timestamp, + time_zone + ) -> None: + + self.params = { + "account_id": account_id, + "endpoint_ids": endpoint_ids, + "UTCTimestamp": utc_timestamp, + "timeZone": time_zone, + } diff --git a/revoking/response.py b/revoking/response.py new file mode 100644 index 00000000..60d0c4bf --- /dev/null +++ b/revoking/response.py @@ -0,0 +1,7 @@ +class RevokingResonse: + """ + Response from revoking request + """ + + def __init__(self, http_response): + self.response = http_response diff --git a/revoking/revoking.py b/revoking/revoking.py new file mode 100644 index 00000000..9d1dc96b --- /dev/null +++ b/revoking/revoking.py @@ -0,0 +1,30 @@ +import requests + +from environments.environmental_services import EnvironmentalService + + +class Revoking(EnvironmentalService): + + def _create_request(self, params: RevokingParameter, url: str) -> RevokingRequest: + body_params = params.get_body_params() + request_body = RevokingBody(**body_params) + + header_params = params.get_header_params() + header_params["request_body"] = request_body.json(new_lines=False) + request_header = RevokingHeader(**header_params) + + return RevokingRequest(header=request_header, body=request_body, url=url) + + def _perform_request(self, params: RevokingParameter, url: str) -> requests.Response: + request = self._create_request(params, url) + return requests.post( + url=request.get_url(), + data=request.get_data(), + headers=request.get_header() + ) + + def revoke(self, params: RevokingParameter) -> RevokingResponse: + url = self._environment.get_revoke_url() + http_response = self._perform_request(params=params, url=url) + + return RevokingResponse(http_response) \ No newline at end of file From e039b7598930561e79371f993e0fc37a44c172a5 Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 12:53:43 +0300 Subject: [PATCH 02/10] Fix import bug in onboarding/onboarding.py --- onboarding/onboarding.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/onboarding/onboarding.py b/onboarding/onboarding.py index 28071c54..24e93d49 100644 --- a/onboarding/onboarding.py +++ b/onboarding/onboarding.py @@ -2,12 +2,10 @@ from environments.environmental_services import EnvironmentalService from onboarding.headers import SoftwareOnboardingHeader, CUOnboardingHeader -from onboarding.parameters import SoftwareOnboardingParameter, BaseOnboardingParameter, RevokingParameter, \ - CUOnboardingParameter +from onboarding.parameters import SoftwareOnboardingParameter, BaseOnboardingParameter, CUOnboardingParameter from onboarding.request import SoftwareOnboardingRequest, BaseOnboardingRequest, CUOnboardingRequest from onboarding.request_body import SoftwareOnboardingBody, CUOnboardingBody -from onboarding.response import SoftwareVerifyOnboardingResponse, SoftwareOnboardingResponse, RevokingResponse, \ - CUOnboardingResponse +from onboarding.response import SoftwareVerifyOnboardingResponse, SoftwareOnboardingResponse, CUOnboardingResponse class SoftwareOnboarding(EnvironmentalService): From eaaacc4d559486366dbab05976e2cfaa3b6c1505 Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 13:14:17 +0300 Subject: [PATCH 03/10] Fix bug in OnboardingParameter.get_body_params() method --- onboarding/parameters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/onboarding/parameters.py b/onboarding/parameters.py index b8a4f879..12f3373a 100644 --- a/onboarding/parameters.py +++ b/onboarding/parameters.py @@ -35,10 +35,10 @@ def __init__(self, self.application_id = application_id self.content_type = content_type self.certification_version_id = certification_version_id - self.gateway_id = gateway_id + self.gateway_id = str(gateway_id) self.certificate_type = certificate_type - self.utc_timestamp = utc_timestamp - self.time_zone = time_zone + self.utc_timestamp = str(utc_timestamp) + self.time_zone = str(time_zone) self.reg_code = reg_code def get_header_params(self): @@ -50,7 +50,7 @@ def get_header_params(self): def get_body_params(self): return { - "id": self.id_, + "id_": self.id_, "application_id": self.application_id, "certification_version_id": self.certification_version_id, "gateway_id": self.gateway_id, @@ -87,7 +87,7 @@ def get_header_params(self): def get_body_params(self): return { - "id": self.id_, + "id_": self.id_, "application_id": self.application_id, "certification_version_id": self.certification_version_id, "gateway_id": self.gateway_id, From db3f2e0e2dc2b82134f43d7751d56312c730dd97 Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 13:15:26 +0300 Subject: [PATCH 04/10] Fix OnboardingParameter.create_request(), OnboardingParameter.create_request() --- onboarding/onboarding.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/onboarding/onboarding.py b/onboarding/onboarding.py index 24e93d49..20365d07 100644 --- a/onboarding/onboarding.py +++ b/onboarding/onboarding.py @@ -15,7 +15,6 @@ def _create_request(self, params: BaseOnboardingParameter, url: str) -> Software request_body = SoftwareOnboardingBody(**body_params) header_params = params.get_header_params() - header_params["request_body"] = request_body.json(new_lines=False) request_header = SoftwareOnboardingHeader(**header_params) return SoftwareOnboardingRequest(header=request_header, body=request_body, url=url) @@ -48,7 +47,6 @@ def _create_request(self, params: CUOnboardingParameter, url: str) -> CUOnboardi request_body = CUOnboardingBody(**body_params) header_params = params.get_header_params() - header_params["request_body"] = request_body.json(new_lines=False) request_header = CUOnboardingHeader(**header_params) return CUOnboardingRequest(header=request_header, body=request_body, url=url) From ad16fc5bd972c77546d7783a3c401eab14438c61 Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 13:40:14 +0300 Subject: [PATCH 05/10] Fix revoking --- revoking/headers.py | 3 +++ revoking/request.py | 6 +++++- revoking/response.py | 2 +- revoking/revoking.py | 6 +++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/revoking/headers.py b/revoking/headers.py index c4d1b7a8..08dc878e 100644 --- a/revoking/headers.py +++ b/revoking/headers.py @@ -13,6 +13,9 @@ def __init__(self, def get_header(self) -> dict: return self.params + def sign(self, signatute): + self.params["X-Agrirouter-Signature"] = signatute + def _set_params(self, application_id: str, signature: str, content_type: str): header = dict() header["Content-Type"] = content_type diff --git a/revoking/request.py b/revoking/request.py index aa7620cc..a08898c4 100644 --- a/revoking/request.py +++ b/revoking/request.py @@ -1,4 +1,8 @@ -class BaseOnboardingRequest: +from revoking.headers import RevokingHeader +from revoking.request_body import RevokingBody + + +class RevokingRequest: def __init__(self, header: RevokingHeader, body: RevokingBody, url: str): self.header = header self.body = body diff --git a/revoking/response.py b/revoking/response.py index 60d0c4bf..249cad11 100644 --- a/revoking/response.py +++ b/revoking/response.py @@ -1,4 +1,4 @@ -class RevokingResonse: +class RevokingResponse: """ Response from revoking request """ diff --git a/revoking/revoking.py b/revoking/revoking.py index 9d1dc96b..8a8a4f7b 100644 --- a/revoking/revoking.py +++ b/revoking/revoking.py @@ -1,6 +1,11 @@ import requests from environments.environmental_services import EnvironmentalService +from revoking.headers import RevokingHeader +from revoking.parameters import RevokingParameter +from revoking.request import RevokingRequest +from revoking.request_body import RevokingBody +from revoking.response import RevokingResponse class Revoking(EnvironmentalService): @@ -10,7 +15,6 @@ def _create_request(self, params: RevokingParameter, url: str) -> RevokingReques request_body = RevokingBody(**body_params) header_params = params.get_header_params() - header_params["request_body"] = request_body.json(new_lines=False) request_header = RevokingHeader(**header_params) return RevokingRequest(header=request_header, body=request_body, url=url) From 9372b544c1dded1926f6ccf923107840ff7c7b0e Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 19:34:37 +0300 Subject: [PATCH 06/10] Implement request signing in onboarding --- onboarding/exceptions.py | 6 ++++++ onboarding/headers.py | 4 ++++ onboarding/onboarding.py | 37 +++++++++++++++++++++++++++---------- onboarding/request.py | 15 +++------------ onboarding/request_body.py | 4 ++++ onboarding/response.py | 22 +++++++++++++++++----- onboarding/signature.py | 2 +- 7 files changed, 62 insertions(+), 28 deletions(-) diff --git a/onboarding/exceptions.py b/onboarding/exceptions.py index 96686763..34dc3777 100644 --- a/onboarding/exceptions.py +++ b/onboarding/exceptions.py @@ -13,3 +13,9 @@ class WrongCertificationType(AgriRouuterBaseException): class WrongGateWay(AgriRouuterBaseException): _message = "Wrong Gate Way Id. Use onboarding.enums.GateWays values instead." + + +class RequestNotSigned(AgriRouuterBaseException): + _message = "Request does not contain signature header. Please sign the request with request.sign() method.\n" \ + "Details on: https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/" \ + "integration/onboarding.html#signing-requests" diff --git a/onboarding/headers.py b/onboarding/headers.py index 32b17a56..8ebf313e 100644 --- a/onboarding/headers.py +++ b/onboarding/headers.py @@ -16,6 +16,10 @@ def get_header(self) -> dict: def _set_params(self, *args, **kwargs): ... + @abstractmethod + def sign(self, *args, **kwargs): + ... + class SoftwareOnboardingHeader(BaseOnboardingHeader): def __init__(self, diff --git a/onboarding/onboarding.py b/onboarding/onboarding.py index 20365d07..ee0d3dfe 100644 --- a/onboarding/onboarding.py +++ b/onboarding/onboarding.py @@ -1,6 +1,7 @@ import requests from environments.environmental_services import EnvironmentalService +from onboarding.exceptions import RequestNotSigned from onboarding.headers import SoftwareOnboardingHeader, CUOnboardingHeader from onboarding.parameters import SoftwareOnboardingParameter, BaseOnboardingParameter, CUOnboardingParameter from onboarding.request import SoftwareOnboardingRequest, BaseOnboardingRequest, CUOnboardingRequest @@ -10,6 +11,11 @@ class SoftwareOnboarding(EnvironmentalService): + def __init__(self, *args, **kwargs): + self._public_key = kwargs.pop("public_key") + self._private_key = kwargs.pop("private_key") + super(SoftwareOnboarding, self).__init__(*args, **kwargs) + def _create_request(self, params: BaseOnboardingParameter, url: str) -> SoftwareOnboardingRequest: body_params = params.get_body_params() request_body = SoftwareOnboardingBody(**body_params) @@ -21,11 +27,14 @@ def _create_request(self, params: BaseOnboardingParameter, url: str) -> Software def _perform_request(self, params: BaseOnboardingParameter, url: str) -> requests.Response: request = self._create_request(params, url) - return requests.post( - url=request.get_url(), - data=request.get_data(), - headers=request.get_header() - ) + request.sign(self._private_key) + if request.is_signed: + return requests.post( + url=request.get_url(), + data=request.get_data(), + headers=request.get_header() + ) + raise RequestNotSigned def verify(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResponse: url = self._environment.get_verify_onboard_request_url() @@ -42,6 +51,11 @@ def onboard(self, params: SoftwareOnboardingParameter) -> SoftwareOnboardingResp class CUOnboarding(EnvironmentalService): + def __init__(self, *args, **kwargs): + self._public_key = kwargs.pop("public_key") + self._private_key = kwargs.pop("private_key") + super(CUOnboarding, self).__init__(*args, **kwargs) + def _create_request(self, params: CUOnboardingParameter, url: str) -> CUOnboardingRequest: body_params = params.get_body_params() request_body = CUOnboardingBody(**body_params) @@ -53,11 +67,14 @@ def _create_request(self, params: CUOnboardingParameter, url: str) -> CUOnboardi def _perform_request(self, params: CUOnboardingParameter, url: str) -> requests.Response: request = self._create_request(params, url) - return requests.post( - url=request.get_url(), - data=request.get_data(), - headers=request.get_header() - ) + request.sign(self._private_key) + if request.is_signed: + return requests.post( + url=request.get_url(), + data=request.get_data(), + headers=request.get_header() + ) + raise RequestNotSigned def onboard(self, params: CUOnboardingParameter) -> CUOnboardingResponse: url = self._environment.get_onboard_url() diff --git a/onboarding/request.py b/onboarding/request.py index 5dda15b5..8ec4258f 100644 --- a/onboarding/request.py +++ b/onboarding/request.py @@ -1,5 +1,6 @@ from onboarding.headers import SoftwareOnboardingHeader, BaseOnboardingHeader from onboarding.request_body import SoftwareOnboardingBody, BaseOnboardingBody +from onboarding.signature import create_signature class BaseOnboardingRequest: @@ -17,14 +18,9 @@ def get_data(self): def get_header(self): return self.header.get_header() - def sign(self): - """ - TODO: add here create_signature - :return: - """ - signature = ... # create signature + def sign(self, private_key): + signature = create_signature(self.body.json(new_lines=False), private_key) self.header.sign(signature) - pass @property def is_signed(self): @@ -33,11 +29,6 @@ def is_signed(self): return True return False - @property - def is_valid(self): - if not self.is_signed: - return False - class SoftwareOnboardingRequest(BaseOnboardingRequest): """ diff --git a/onboarding/request_body.py b/onboarding/request_body.py index b4f922b6..1d6c2ff2 100644 --- a/onboarding/request_body.py +++ b/onboarding/request_body.py @@ -18,6 +18,10 @@ def get_parameters(self, *args, **kwargs) -> dict: def _set_params(self, *args, **kwargs): ... + @abstractmethod + def json(self, *args, **kwargs): + ... + class SoftwareOnboardingBody(BaseOnboardingBody): def __init__(self, diff --git a/onboarding/response.py b/onboarding/response.py index e1d1d420..4718b641 100644 --- a/onboarding/response.py +++ b/onboarding/response.py @@ -1,10 +1,22 @@ -from abc import ABC, abstractmethod +from requests import Response -class BaseOnboardingResonse(ABC): - @abstractmethod - def __init__(self, http_response): - self.response = http_response +class BaseOnboardingResonse: + + def __init__(self, http_response: Response): + self.response: Response = http_response + + @property + def data(self): + return self.response.json() + + @property + def status_code(self): + return self.response.status_code + + @property + def text(self): + return self.response.text class SoftwareVerifyOnboardingResponse(BaseOnboardingResonse): diff --git a/onboarding/signature.py b/onboarding/signature.py index fa68befc..6bedc3bd 100644 --- a/onboarding/signature.py +++ b/onboarding/signature.py @@ -18,7 +18,7 @@ def create_signature(request_body: str, private_key: str) -> bytes: return signature -def verify_signature(request_body: str, signature: bytearray, public_key: str) -> None: +def verify_signature(request_body: str, signature: bytes, public_key: str) -> None: public_key_bytes = bytearray(public_key.encode('utf-8')) public_key_data = load_pem_public_key(public_key_bytes) public_key_data.verify( From 810cb1895384be91db57e85b452abab9f0d9dcf2 Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 19:34:52 +0300 Subject: [PATCH 07/10] Implement request signing in revoking --- revoking/request.py | 9 +++------ revoking/request_body.py | 9 +++++++++ revoking/response.py | 19 +++++++++++++++++-- revoking/revoking.py | 19 ++++++++++++++----- 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/revoking/request.py b/revoking/request.py index a08898c4..df80233e 100644 --- a/revoking/request.py +++ b/revoking/request.py @@ -1,3 +1,4 @@ +from onboarding.signature import create_signature from revoking.headers import RevokingHeader from revoking.request_body import RevokingBody @@ -17,12 +18,8 @@ def get_data(self): def get_header(self): return self.header.get_header() - def sign(self): - """ - TODO: add here create_signature - :return: - """ - signature = ... # create signature + def sign(self, private_key): + signature = create_signature(self.body.json(new_lines=False), private_key) self.header.sign(signature) @property diff --git a/revoking/request_body.py b/revoking/request_body.py index e23c2750..6ec70b2d 100644 --- a/revoking/request_body.py +++ b/revoking/request_body.py @@ -1,3 +1,6 @@ +import json + + class RevokingBody: def __init__(self, account_id, @@ -28,3 +31,9 @@ def _set_params(self, "UTCTimestamp": utc_timestamp, "timeZone": time_zone, } + + def json(self, new_lines: bool = True) -> str: + result = json.dumps(self.get_parameters(), indent="") + if not new_lines: + return result.replace("\n", "") + return result diff --git a/revoking/response.py b/revoking/response.py index 249cad11..cec8bb00 100644 --- a/revoking/response.py +++ b/revoking/response.py @@ -1,7 +1,22 @@ +from requests import Response + + class RevokingResponse: """ Response from revoking request """ - def __init__(self, http_response): - self.response = http_response + def __init__(self, http_response: Response): + self.response: Response = http_response + + @property + def data(self): + return self.response.json() + + @property + def status_code(self): + return self.response.status_code + + @property + def text(self): + return self.response.text diff --git a/revoking/revoking.py b/revoking/revoking.py index 8a8a4f7b..a1adb219 100644 --- a/revoking/revoking.py +++ b/revoking/revoking.py @@ -1,6 +1,7 @@ import requests from environments.environmental_services import EnvironmentalService +from onboarding.exceptions import RequestNotSigned from revoking.headers import RevokingHeader from revoking.parameters import RevokingParameter from revoking.request import RevokingRequest @@ -10,6 +11,11 @@ class Revoking(EnvironmentalService): + def __init__(self, *args, **kwargs): + self._public_key = kwargs.pop("public_key") + self._private_key = kwargs.pop("private_key") + super(Revoking, self).__init__(*args, **kwargs) + def _create_request(self, params: RevokingParameter, url: str) -> RevokingRequest: body_params = params.get_body_params() request_body = RevokingBody(**body_params) @@ -21,11 +27,14 @@ def _create_request(self, params: RevokingParameter, url: str) -> RevokingReques def _perform_request(self, params: RevokingParameter, url: str) -> requests.Response: request = self._create_request(params, url) - return requests.post( - url=request.get_url(), - data=request.get_data(), - headers=request.get_header() - ) + request.sign(self._private_key) + if request.is_signed: + return requests.post( + url=request.get_url(), + data=request.get_data(), + headers=request.get_header() + ) + raise RequestNotSigned def revoke(self, params: RevokingParameter) -> RevokingResponse: url = self._environment.get_revoke_url() From 0fd281e1c95bc094fba1327df67420991c1893cd Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Tue, 28 Sep 2021 19:35:04 +0300 Subject: [PATCH 08/10] Implement request signing in auth --- auth/auth.py | 10 ++++++++-- auth/response.py | 21 ++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/auth/auth.py b/auth/auth.py index 6d23e662..d0525e82 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -12,6 +12,11 @@ class Authorization(EnvironmentalService): TOKEN_KEY = "token" ERROR_KEY = "error" + def __init__(self, *args, **kwargs): + self._public_key = kwargs.pop("public_key") + self._private_key = kwargs.pop("private_key") + super(Authorization, self).__init__(*args, **kwargs) + def get_auth_request_url(self, parameters: AuthUrlParameter) -> str: auth_parameters = parameters.get_parameters() return self._environment.get_secured_onboarding_authorization_url(**auth_parameters) @@ -21,8 +26,9 @@ def extract_auth_response(self, url: str) -> AuthResponse: query_params = self._extract_query_params(parsed_url.query) return AuthResponse(query_params) - def verify_auth_response(self): - pass + @staticmethod + def verify_auth_response(response, public_key): + response.verify(public_key) @staticmethod def _extract_query_params(query_params: str) -> dict: diff --git a/auth/response.py b/auth/response.py index e67f2b8e..40a6b9c3 100644 --- a/auth/response.py +++ b/auth/response.py @@ -1,6 +1,11 @@ import base64 import json from typing import Union +from urllib.parse import unquote + +from cryptography.exceptions import InvalidSignature + +from onboarding.signature import verify_signature class AuthResponse: @@ -19,7 +24,7 @@ def __init__(self, query_params): self.is_successful = not bool(self._error) self.is_valid = False - def verify(self) -> None: + def verify(self, public_key) -> None: """ Validates signature according to docs: https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/integration/authorization.html#analyse-result @@ -29,17 +34,23 @@ def verify(self) -> None: :return: """ - # TODO: implement validation of response according to docs: - # https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/integration/authorization.html#analyse-result + encoded_data = self._state + self._token + unquoted_signature = unquote(self._signature) + encoded_signature = base64.b64decode(unquoted_signature.encode("utf-8")) + try: + verify_signature(encoded_data, encoded_signature, public_key) + except InvalidSignature: + print("Response is invalid: invalid signature.") + self.is_valid = False self.is_valid = True @staticmethod def decode_token(token: Union[str, bytes]) -> dict: if type(token) == str: - token = token.encode("ASCII") + token = token.encode("utf-8") base_64_decoded_token = base64.b64decode(token) - decoded_token = base_64_decoded_token.decode("ASCII") + decoded_token = base_64_decoded_token.decode("utf-8") return json.loads(decoded_token) def get_auth_result(self) -> dict: From 65f5662dad3564bc4f487b5fd53bcf854571f07c Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Wed, 29 Sep 2021 13:40:28 +0300 Subject: [PATCH 09/10] Refactor RequestNotSigned exception --- onboarding/exceptions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/onboarding/exceptions.py b/onboarding/exceptions.py index 34dc3777..490e51e3 100644 --- a/onboarding/exceptions.py +++ b/onboarding/exceptions.py @@ -16,6 +16,8 @@ class WrongGateWay(AgriRouuterBaseException): class RequestNotSigned(AgriRouuterBaseException): - _message = "Request does not contain signature header. Please sign the request with request.sign() method.\n" \ - "Details on: https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/" \ - "integration/onboarding.html#signing-requests" + _message = """ + Request does not contain signature header. Please sign the request with request.sign() method.\n + Details on: https://docs.my-agrirouter.com/agrirouter-interface-documentation/latest/ + integration/onboarding.html#signing-requests + """ From d96a4f3fec53a47f22caf7a198d6ff4543074a34 Mon Sep 17 00:00:00 2001 From: Alexey Petrovsky Date: Wed, 29 Sep 2021 13:53:38 +0300 Subject: [PATCH 10/10] Create requeirements.txt --- requirements.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e2da2ee2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +certifi==2021.5.30 +cffi==1.14.6 +charset-normalizer==2.0.6 +cryptography==3.4.8 +idna==3.2 +pycparser==2.20 +requests==2.26.0 +urllib3==1.26.7