def get_certificate(self):
        """
        Return the server verification certificate from the trust bundle that can be used to
        validate the server-side SSL TLS connection that we use to talk to Edge

        :return: The server verification certificate to use for connections to the Azure IoT Edge
        instance, as a PEM certificate in string form.

        :raises: IoTEdgeError if unable to retrieve the certificate.
        """
        r = requests.get(
            self.workload_uri + "trust-bundle",
            params={"api-version": self.api_version},
            headers={
                "User-Agent":
                urllib.parse.quote_plus(user_agent.get_iothub_user_agent())
            },
        )
        # Validate that the request was successful
        try:
            r.raise_for_status()
        except requests.exceptions.HTTPError as e:
            raise IoTEdgeError("Unable to get trust bundle from Edge") from e
        # Decode the trust bundle
        try:
            bundle = r.json()
        except ValueError as e:
            raise IoTEdgeError("Unable to decode trust bundle") from e
        # Retrieve the certificate
        try:
            cert = bundle["certificate"]
        except KeyError as e:
            raise IoTEdgeError("No certificate in trust bundle") from e
        return cert
Example #2
0
    def test_requests_data_signing(self, mocker, edge_hsm):
        data_str = "somedata"
        data_str_b64 = "c29tZWRhdGE="
        mock_request_post = mocker.patch.object(requests, "post")
        mock_request_post.return_value.json.return_value = {
            "digest": "somedigest"
        }
        expected_url = "{workload_uri}modules/{module_id}/genid/{generation_id}/sign".format(
            workload_uri=edge_hsm.workload_uri,
            module_id=edge_hsm.module_id,
            generation_id=edge_hsm.generation_id,
        )
        expected_params = {"api-version": edge_hsm.api_version}
        expected_headers = {
            "User-Agent":
            urllib.parse.quote(user_agent.get_iothub_user_agent(), safe="")
        }
        expected_json = json.dumps({
            "keyId": "primary",
            "algo": "HMACSHA256",
            "data": data_str_b64
        })

        edge_hsm.sign(data_str)

        assert mock_request_post.call_count == 1
        assert mock_request_post.call_args == mocker.call(
            url=expected_url,
            params=expected_params,
            headers=expected_headers,
            data=expected_json)
    def test_username(self, stage, op, pipeline_config, cust_product_info):
        pipeline_config.product_info = cust_product_info
        assert not hasattr(op, "username")
        stage.run_op(op)

        expected_username = "******".format(
            hostname=pipeline_config.hostname,
            client_id=pipeline_config.device_id,
            api_version=pkg_constant.IOTHUB_API_VERSION,
            user_agent=urllib.parse.quote(user_agent.get_iothub_user_agent(), safe=""),
            custom_product_info=urllib.parse.quote(pipeline_config.product_info, safe=""),
        )
        assert op.username == expected_username
    def test_username_for_digital_twin(self, stage, op, pipeline_config, digital_twin_product_info):
        pipeline_config.product_info = digital_twin_product_info
        assert not hasattr(op, "username")
        stage.run_op(op)

        expected_username = "******".format(
            hostname=pipeline_config.hostname,
            client_id=pipeline_config.device_id,
            api_version=pkg_constant.DIGITAL_TWIN_API_VERSION,
            user_agent=urllib.parse.quote(user_agent.get_iothub_user_agent(), safe=""),
            digital_twin_prefix=pkg_constant.DIGITAL_TWIN_QUERY_HEADER,
            custom_product_info=urllib.parse.quote(pipeline_config.product_info, safe=""),
        )
        assert op.username == expected_username
Example #5
0
    def test_requests_trust_bundle(self, mocker, edge_hsm):
        mock_request_get = mocker.patch.object(requests, "get")
        expected_url = edge_hsm.workload_uri + "trust-bundle"
        expected_params = {"api-version": edge_hsm.api_version}
        expected_headers = {
            "User-Agent":
            urllib.parse.quote_plus(user_agent.get_iothub_user_agent())
        }

        edge_hsm.get_certificate()

        assert mock_request_get.call_count == 1
        assert mock_request_get.call_args == mocker.call(
            expected_url, params=expected_params, headers=expected_headers)
    def sign(self, data_str):
        """
        Use the IoTEdge HSM to sign a piece of string data.  The caller should then insert the
        returned value (the signature) into the 'sig' field of a SharedAccessSignature string.

        :param str data_str: The data string to sign

        :return: The signature, as a URI-encoded and base64-encoded value that is ready to
        directly insert into the SharedAccessSignature string.

        :raises: IoTEdgeError if unable to sign the data.
        """
        encoded_data_str = base64.b64encode(data_str.encode("utf-8")).decode()

        path = "{workload_uri}modules/{module_id}/genid/{gen_id}/sign".format(
            workload_uri=self.workload_uri,
            module_id=self.module_id,
            gen_id=self.generation_id)
        sign_request = {
            "keyId": "primary",
            "algo": "HMACSHA256",
            "data": encoded_data_str
        }

        r = requests.post(  # can we use json field instead of data?
            url=path,
            params={"api-version": self.api_version},
            headers={
                "User-Agent":
                urllib.parse.quote(user_agent.get_iothub_user_agent(), safe="")
            },
            data=json.dumps(sign_request),
        )
        try:
            r.raise_for_status()
        except requests.exceptions.HTTPError as e:
            raise IoTEdgeError("Unable to sign data") from e
        try:
            sign_response = r.json()
        except ValueError as e:
            raise IoTEdgeError("Unable to decode signed data") from e
        try:
            signed_data_str = sign_response["digest"]
        except KeyError as e:
            raise IoTEdgeError("No signed data received") from e

        return signed_data_str  # what format is this? string? bytes?
    def test_get_iothub_user_agent(self):
        user_agent_str = user_agent.get_iothub_user_agent()

        assert IOTHUB_IDENTIFIER in user_agent_str
        assert VERSION in user_agent_str
        assert platform.python_version() in user_agent_str
        assert platform.system() in user_agent_str
        assert platform.version() in user_agent_str
        assert platform.machine() in user_agent_str
        expected_part_agent = check_agent_format.format(
            identifier=IOTHUB_IDENTIFIER,
            version=VERSION,
            python_runtime=platform.python_version(),
            os_type=platform.system(),
            os_release=platform.version(),
            architecture=platform.machine(),
        )
        assert expected_part_agent == user_agent_str
Example #8
0
    def test_new_op_headers(self, mocker, stage, op, custom_user_agent,
                            pipeline_config):
        stage.pipeline_root.pipeline_configuration.product_info = custom_user_agent
        stage.run_op(op)

        # Op was sent down
        assert stage.send_op_down.call_count == 1
        new_op = stage.send_op_down.call_args[0][0]
        assert isinstance(new_op,
                          pipeline_ops_http.HTTPRequestAndResponseOperation)

        # Validate headers
        expected_user_agent = urllib.parse.quote_plus(
            user_agent.get_iothub_user_agent() + str(custom_user_agent))

        assert new_op.headers["Host"] == pipeline_config.hostname
        assert new_op.headers[
            "Content-Type"] == "application/json; charset=utf-8"
        assert new_op.headers["Content-Length"] == len(new_op.body)
        assert new_op.headers["User-Agent"] == expected_user_agent
    def test_new_op_headers(self, mocker, stage, op, custom_user_agent,
                            pipeline_config):
        stage.pipeline_root.pipeline_configuration.product_info = custom_user_agent
        stage.run_op(op)

        # Op was sent down
        assert stage.send_op_down.call_count == 1
        new_op = stage.send_op_down.call_args[0][0]
        assert isinstance(new_op,
                          pipeline_ops_http.HTTPRequestAndResponseOperation)

        # Validate headers
        expected_user_agent = urllib.parse.quote_plus(
            user_agent.get_iothub_user_agent() + str(custom_user_agent))
        expected_edge_string = "{}/{}".format(pipeline_config.device_id,
                                              pipeline_config.module_id)

        assert new_op.headers["Host"] == pipeline_config.gateway_hostname
        assert new_op.headers["Content-Type"] == "application/json"
        assert new_op.headers["Content-Length"] == str(len(new_op.body))
        assert new_op.headers["x-ms-edge-moduleId"] == expected_edge_string
        assert new_op.headers["User-Agent"] == expected_user_agent
    def _run_op(self, op):

        if isinstance(op, pipeline_ops_base.InitializePipelineOperation):

            if self.pipeline_root.pipeline_configuration.module_id:
                # Module Format
                client_id = "{}/{}".format(
                    self.pipeline_root.pipeline_configuration.device_id,
                    self.pipeline_root.pipeline_configuration.module_id,
                )
            else:
                # Device Format
                client_id = self.pipeline_root.pipeline_configuration.device_id

            query_param_seq = []

            # Apply query parameters (i.e. key1=value1&key2=value2...&keyN=valueN format)
            custom_product_info = str(
                self.pipeline_root.pipeline_configuration.product_info)
            if custom_product_info.startswith(
                    pkg_constant.DIGITAL_TWIN_PREFIX):  # Digital Twin Stuff
                query_param_seq.append(
                    ("api-version", pkg_constant.DIGITAL_TWIN_API_VERSION))
                query_param_seq.append(
                    ("DeviceClientType", user_agent.get_iothub_user_agent()))
                query_param_seq.append((pkg_constant.DIGITAL_TWIN_QUERY_HEADER,
                                        custom_product_info))
            else:
                query_param_seq.append(
                    ("api-version", pkg_constant.IOTHUB_API_VERSION))
                query_param_seq.append(
                    ("DeviceClientType",
                     user_agent.get_iothub_user_agent() + custom_product_info))

            # NOTE: Client ID (including the device and/or module ids that are in it)
            # is NOT url encoded as part of the username. Neither is the hostname.
            # The sequence of key/value property pairs (query_param_seq) however, MUST have all
            # keys and values URL encoded.
            # See the repo wiki article for details:
            # https://github.com/Azure/azure-iot-sdk-python/wiki/URL-Encoding-(MQTT)
            username = "******".format(
                hostname=self.pipeline_root.pipeline_configuration.hostname,
                client_id=client_id,
                query_params=version_compat.urlencode(
                    query_param_seq, quote_via=urllib.parse.quote),
            )

            # Dynamically attach the derived MQTT values to the InitalizePipelineOperation
            # to be used later down the pipeline
            op.username = username
            op.client_id = client_id

            self.send_op_down(op)

        elif isinstance(
                op, pipeline_ops_iothub.SendD2CMessageOperation) or isinstance(
                    op, pipeline_ops_iothub.SendOutputMessageOperation):
            # Convert SendTelementry and SendOutputMessageOperation operations into MQTT Publish operations
            telemetry_topic = mqtt_topic_iothub.get_telemetry_topic_for_publish(
                device_id=self.pipeline_root.pipeline_configuration.device_id,
                module_id=self.pipeline_root.pipeline_configuration.module_id,
            )
            topic = mqtt_topic_iothub.encode_message_properties_in_topic(
                op.message, telemetry_topic)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTPublishOperation,
                topic=topic,
                payload=op.message.data,
            )
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_iothub.SendMethodResponseOperation):
            # Sending a Method Response gets translated into an MQTT Publish operation
            topic = mqtt_topic_iothub.get_method_topic_for_publish(
                op.method_response.request_id, op.method_response.status)
            payload = json.dumps(op.method_response.payload)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTPublishOperation,
                topic=topic,
                payload=payload)
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_base.EnableFeatureOperation):
            # Enabling a feature gets translated into an MQTT subscribe operation
            topic = self._get_feature_subscription_topic(op.feature_name)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTSubscribeOperation,
                topic=topic)
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_base.DisableFeatureOperation):
            # Disabling a feature gets turned into an MQTT unsubscribe operation
            topic = self._get_feature_subscription_topic(op.feature_name)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTUnsubscribeOperation,
                topic=topic)
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_base.RequestOperation):
            if op.request_type == pipeline_constant.TWIN:
                topic = mqtt_topic_iothub.get_twin_topic_for_publish(
                    method=op.method,
                    resource_location=op.resource_location,
                    request_id=op.request_id,
                )
                worker_op = op.spawn_worker_op(
                    worker_op_type=pipeline_ops_mqtt.MQTTPublishOperation,
                    topic=topic,
                    payload=op.request_body,
                )
                self.send_op_down(worker_op)
            else:
                raise pipeline_exceptions.OperationError(
                    "RequestOperation request_type {} not supported".format(
                        op.request_type))

        else:
            # All other operations get passed down
            super(IoTHubMQTTTranslationStage, self)._run_op(op)
Example #11
0
    def _run_op(self, op):

        if isinstance(op, pipeline_ops_base.InitializePipelineOperation):

            if self.pipeline_root.pipeline_configuration.module_id:
                # Module Format
                client_id = "{}/{}".format(
                    self.pipeline_root.pipeline_configuration.device_id,
                    self.pipeline_root.pipeline_configuration.module_id,
                )
            else:
                # Device Format
                client_id = self.pipeline_root.pipeline_configuration.device_id

            # Apply query parameters (i.e. key1=value1&key2=value2...&keyN=valueN format)
            query_param_seq = [
                ("api-version", pkg_constant.IOTHUB_API_VERSION),
                (
                    "DeviceClientType",
                    user_agent.get_iothub_user_agent() +
                    self.pipeline_root.pipeline_configuration.product_info,
                ),
            ]
            username = "******".format(
                hostname=self.pipeline_root.pipeline_configuration.hostname,
                client_id=client_id,
                query_params=version_compat.urlencode(
                    query_param_seq, quote_via=urllib.parse.quote),
            )

            # Dynamically attach the derived MQTT values to the InitalizePipelineOperation
            # to be used later down the pipeline
            op.username = username
            op.client_id = client_id

            self.send_op_down(op)

        elif isinstance(
                op, pipeline_ops_iothub.SendD2CMessageOperation) or isinstance(
                    op, pipeline_ops_iothub.SendOutputMessageOperation):
            # Convert SendTelementry and SendOutputMessageOperation operations into MQTT Publish operations
            telemetry_topic = mqtt_topic_iothub.get_telemetry_topic_for_publish(
                device_id=self.pipeline_root.pipeline_configuration.device_id,
                module_id=self.pipeline_root.pipeline_configuration.module_id,
            )
            topic = mqtt_topic_iothub.encode_message_properties_in_topic(
                op.message, telemetry_topic)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTPublishOperation,
                topic=topic,
                payload=op.message.data,
            )
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_iothub.SendMethodResponseOperation):
            # Sending a Method Response gets translated into an MQTT Publish operation
            topic = mqtt_topic_iothub.get_method_topic_for_publish(
                op.method_response.request_id, op.method_response.status)
            payload = json.dumps(op.method_response.payload)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTPublishOperation,
                topic=topic,
                payload=payload)
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_base.EnableFeatureOperation):
            # Enabling a feature gets translated into an MQTT subscribe operation
            topic = self._get_feature_subscription_topic(op.feature_name)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTSubscribeOperation,
                topic=topic)
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_base.DisableFeatureOperation):
            # Disabling a feature gets turned into an MQTT unsubscribe operation
            topic = self._get_feature_subscription_topic(op.feature_name)
            worker_op = op.spawn_worker_op(
                worker_op_type=pipeline_ops_mqtt.MQTTUnsubscribeOperation,
                topic=topic)
            self.send_op_down(worker_op)

        elif isinstance(op, pipeline_ops_base.RequestOperation):
            if op.request_type == pipeline_constant.TWIN:
                topic = mqtt_topic_iothub.get_twin_topic_for_publish(
                    method=op.method,
                    resource_location=op.resource_location,
                    request_id=op.request_id,
                )
                worker_op = op.spawn_worker_op(
                    worker_op_type=pipeline_ops_mqtt.MQTTPublishOperation,
                    topic=topic,
                    payload=op.request_body,
                )
                self.send_op_down(worker_op)
            else:
                raise pipeline_exceptions.OperationError(
                    "RequestOperation request_type {} not supported".format(
                        op.request_type))

        else:
            # All other operations get passed down
            super(IoTHubMQTTTranslationStage, self)._run_op(op)
    def _run_op(self, op):
        if isinstance(op, pipeline_ops_iothub_http.MethodInvokeOperation):
            logger.debug(
                "{}({}): Translating Method Invoke Operation for HTTP.".format(
                    self.name, op.name))
            query_params = "api-version={apiVersion}".format(
                apiVersion=pkg_constant.IOTHUB_API_VERSION)
            #  if the target is a module.

            body = json.dumps(op.method_params)
            path = http_path_iothub.get_method_invoke_path(
                op.target_device_id, op.target_module_id)
            # NOTE: we do not add the sas Authorization header here. Instead we add it later on in
            # the HTTPTransportStage
            x_ms_edge_string = "{deviceId}/{moduleId}".format(
                deviceId=self.pipeline_root.pipeline_configuration.device_id,
                moduleId=self.pipeline_root.pipeline_configuration.module_id,
            )  # these are the identifiers of the current module
            user_agent_string = urllib.parse.quote_plus(
                user_agent.get_iothub_user_agent() +
                str(self.pipeline_root.pipeline_configuration.product_info))
            # Method Invoke must be addressed to the gateway hostname because it is an Edge op
            headers = {
                "Host":
                self.pipeline_root.pipeline_configuration.gateway_hostname,
                "Content-Type": "application/json",
                "Content-Length": str(len(str(body))),
                "x-ms-edge-moduleId": x_ms_edge_string,
                "User-Agent": user_agent_string,
            }
            op_waiting_for_response = op

            def on_request_response(op, error):
                logger.debug(
                    "{}({}): Got response for MethodInvokeOperation".format(
                        self.name, op.name))
                error = map_http_error(error=error, http_op=op)
                if not error:
                    op_waiting_for_response.method_response = json.loads(
                        op.response_body)
                op_waiting_for_response.complete(error=error)

            self.send_op_down(
                pipeline_ops_http.HTTPRequestAndResponseOperation(
                    method="POST",
                    path=path,
                    headers=headers,
                    body=body,
                    query_params=query_params,
                    callback=on_request_response,
                ))

        elif isinstance(op, pipeline_ops_iothub_http.GetStorageInfoOperation):
            logger.debug(
                "{}({}): Translating Get Storage Info Operation to HTTP.".
                format(self.name, op.name))
            query_params = "api-version={apiVersion}".format(
                apiVersion=pkg_constant.IOTHUB_API_VERSION)
            path = http_path_iothub.get_storage_info_for_blob_path(
                self.pipeline_root.pipeline_configuration.device_id)
            body = json.dumps({"blobName": op.blob_name})
            user_agent_string = urllib.parse.quote_plus(
                user_agent.get_iothub_user_agent() +
                str(self.pipeline_root.pipeline_configuration.product_info))
            headers = {
                "Host": self.pipeline_root.pipeline_configuration.hostname,
                "Accept": "application/json",
                "Content-Type": "application/json",
                "Content-Length": str(len(str(body))),
                "User-Agent": user_agent_string,
            }

            op_waiting_for_response = op

            def on_request_response(op, error):
                logger.debug(
                    "{}({}): Got response for GetStorageInfoOperation".format(
                        self.name, op.name))
                error = map_http_error(error=error, http_op=op)
                if not error:
                    op_waiting_for_response.storage_info = json.loads(
                        op.response_body)
                op_waiting_for_response.complete(error=error)

            self.send_op_down(
                pipeline_ops_http.HTTPRequestAndResponseOperation(
                    method="POST",
                    path=path,
                    headers=headers,
                    body=body,
                    query_params=query_params,
                    callback=on_request_response,
                ))

        elif isinstance(
                op, pipeline_ops_iothub_http.NotifyBlobUploadStatusOperation):
            logger.debug(
                "{}({}): Translating Get Storage Info Operation to HTTP.".
                format(self.name, op.name))
            query_params = "api-version={apiVersion}".format(
                apiVersion=pkg_constant.IOTHUB_API_VERSION)
            path = http_path_iothub.get_notify_blob_upload_status_path(
                self.pipeline_root.pipeline_configuration.device_id)
            body = json.dumps({
                "correlationId": op.correlation_id,
                "isSuccess": op.is_success,
                "statusCode": op.request_status_code,
                "statusDescription": op.status_description,
            })
            user_agent_string = urllib.parse.quote_plus(
                user_agent.get_iothub_user_agent() +
                str(self.pipeline_root.pipeline_configuration.product_info))

            # NOTE we do not add the sas Authorization header here. Instead we add it later on in
            # the HTTPTransportStage
            headers = {
                "Host": self.pipeline_root.pipeline_configuration.hostname,
                "Content-Type": "application/json; charset=utf-8",
                "Content-Length": str(len(str(body))),
                "User-Agent": user_agent_string,
            }
            op_waiting_for_response = op

            def on_request_response(op, error):
                logger.debug(
                    "{}({}): Got response for GetStorageInfoOperation".format(
                        self.name, op.name))
                error = map_http_error(error=error, http_op=op)
                op_waiting_for_response.complete(error=error)

            self.send_op_down(
                pipeline_ops_http.HTTPRequestAndResponseOperation(
                    method="POST",
                    path=path,
                    headers=headers,
                    body=body,
                    query_params=query_params,
                    callback=on_request_response,
                ))

        else:
            # All other operations get passed down
            self.send_op_down(op)