Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for TPM Using PyOpenSSL with Azure IoT SDK #1194

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion azure-iot-device/azure/iot/device/common/http_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(
hostname,
server_verification_cert=None,
x509_cert=None,
ssl_context=None,
cipher=None,
proxy_options=None,
):
Expand All @@ -37,11 +38,13 @@ def __init__(
:param str server_verification_cert: Certificate which can be used to validate a server-side TLS connection (optional).
:param str cipher: Cipher string in OpenSSL cipher list format (optional)
:param x509_cert: Certificate which can be used to authenticate connection to a server in lieu of a password (optional).
:param ssl_context: Use a custom ssl_context (optional).
:param proxy_options: Options for sending traffic through proxy servers.
"""
self._hostname = hostname
self._server_verification_cert = server_verification_cert
self._x509_cert = x509_cert
self._ssl_context = ssl_context
self._cipher = cipher
self._proxies = format_proxies(proxy_options)
self._http_adapter = self._create_http_adapter()
Expand All @@ -51,7 +54,11 @@ def _create_http_adapter(self):
This method creates a custom HTTPAdapter for use with a requests library session.
It will allow for use of a custom configured SSL context.
"""
ssl_context = self._create_ssl_context()

if self._ssl_context is None:
ssl_context = self._create_ssl_context()
else:
ssl_context = self._ssl_context

class CustomSSLContextHTTPAdapter(requests.adapters.HTTPAdapter):
def init_poolmanager(self, *args, **kwargs):
Expand Down
8 changes: 7 additions & 1 deletion azure-iot-device/azure/iot/device/common/mqtt_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def __init__(
username,
server_verification_cert=None,
x509_cert=None,
ssl_context=None,
websockets=False,
cipher=None,
proxy_options=None,
Expand All @@ -106,6 +107,7 @@ def __init__(
:param str username: Username for login to the remote broker.
:param str server_verification_cert: Certificate which can be used to validate a server-side TLS connection (optional).
:param x509_cert: Certificate which can be used to authenticate connection to a server in lieu of a password (optional).
:param ssl_context: Use a custom ssl_context (optional).
:param bool websockets: Indicates whether or not to enable a websockets connection in the Transport.
:param str cipher: Cipher string in OpenSSL cipher list format
:param proxy_options: Options for sending traffic through proxy servers.
Expand All @@ -116,6 +118,7 @@ def __init__(
self._mqtt_client = None
self._server_verification_cert = server_verification_cert
self._x509_cert = x509_cert
self._ssl_context = ssl_context
self._websockets = websockets
self._cipher = cipher
self._proxy_options = proxy_options
Expand Down Expand Up @@ -165,7 +168,10 @@ def _create_mqtt_client(self):
mqtt_client.enable_logger(logging.getLogger("paho"))

# Configure TLS/SSL
ssl_context = self._create_ssl_context()
if self._ssl_context is None:
ssl_context = self._create_ssl_context()
else:
ssl_context = self._ssl_context
mqtt_client.tls_set_context(context=ssl_context)

# Set event handlers. Use weak references back into this object to prevent leaks
Expand Down
6 changes: 4 additions & 2 deletions azure-iot-device/azure/iot/device/common/pipeline/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def __init__(
gateway_hostname=None,
sastoken=None,
x509=None,
ssl_context=None,
server_verification_cert=None,
websockets=False,
cipher="",
Expand Down Expand Up @@ -69,8 +70,9 @@ def __init__(
# Auth
self.sastoken = sastoken
self.x509 = x509
if (not sastoken and not x509) or (sastoken and x509):
raise ValueError("One of either 'sastoken' or 'x509' must be provided")
self.ssl_context = ssl_context
if (not sastoken and not x509 and not ssl_context) or (sastoken and x509):
raise ValueError("One of either 'sastoken' or 'x509', 'ssl_context' must be provided")
self.server_verification_cert = server_verification_cert
self.websockets = websockets
self.cipher = self._sanitize_cipher(cipher)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def _run_op(self, op):
hostname=hostname,
server_verification_cert=self.nucleus.pipeline_configuration.server_verification_cert,
x509_cert=self.nucleus.pipeline_configuration.x509,
ssl_context=self.nucleus.pipeline_configuration.ssl_context,
cipher=self.nucleus.pipeline_configuration.cipher,
proxy_options=self.nucleus.pipeline_configuration.proxy_options,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ def _run_op(self, op):
username=op.username,
server_verification_cert=self.nucleus.pipeline_configuration.server_verification_cert,
x509_cert=self.nucleus.pipeline_configuration.x509,
ssl_context=self.nucleus.pipeline_configuration.ssl_context,
websockets=self.nucleus.pipeline_configuration.websockets,
cipher=self.nucleus.pipeline_configuration.cipher,
proxy_options=self.nucleus.pipeline_configuration.proxy_options,
Expand Down
129 changes: 129 additions & 0 deletions azure-iot-device/azure/iot/device/iothub/abstract_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import os
import io
import time
import ssl
import OpenSSL

from . import pipeline
from .pipeline import constant as pipeline_constant
from azure.iot.device.common.auth import connection_string as cs
Expand Down Expand Up @@ -615,6 +618,66 @@ def create_from_x509_certificate(

return cls(mqtt_pipeline, http_pipeline)

@classmethod
def create_from_ssl_context(
cls,
ssl_context: ssl.SSLContext | OpenSSL.SSL.Context,
hostname: str,
device_id: str,
**kwargs,
) -> Self:
"""
Instantiate a client using X509 certificate authentication.

:param str hostname: Host running the IotHub.
Can be found in the Azure portal in the Overview tab as the string hostname.
:param ssl_context: Custom ssl.SSLContext
:param str device_id: The ID used to uniquely identify a device in the IoTHub

:param str server_verification_cert: Configuration Option. The trusted certificate chain.
Necessary when using connecting to an endpoint which has a non-standard root of trust,
such as a protocol gateway.
:param str gateway_hostname: Configuration Option. The gateway hostname for the gateway
device.
:param bool websockets: Configuration Option. Default is False. Set to true if using MQTT
over websockets.
:param str product_info: Configuration Option. Default is empty string. The string contains
arbitrary product info which is appended to the user agent string.
:param proxy_options: Options for sending traffic through proxy servers.
:type proxy_options: :class:`azure.iot.device.ProxyOptions`
:param int keep_alive: Maximum period in seconds between communications with the
broker. If no other messages are being exchanged, this controls the
rate at which the client will send ping messages to the broker.
If not provided default value of 60 secs will be used.
:param bool auto_connect: Automatically connect the client to IoTHub when a method is
invoked which requires a connection to be established. (Default: True)
:param bool connection_retry: Attempt to re-establish a dropped connection (Default: True)
:param int connection_retry_interval: Interval, in seconds, between attempts to
re-establish a dropped connection (Default: 10)
:param bool ensure_desired_properties: Ensure the most recent desired properties patch has
been received upon re-connections (Default:True)

:raises: TypeError if given an unsupported parameter.

:returns: An instance of an IoTHub client that uses an X509 certificate for authentication.
"""
# Ensure no invalid kwargs were passed by the user
excluded_kwargs = ["sastoken_ttl"]
_validate_kwargs(exclude=excluded_kwargs, **kwargs)

# Pipeline Config setup
config_kwargs = _get_config_kwargs(**kwargs)
pipeline_configuration = pipeline.IoTHubPipelineConfig(
device_id=device_id, hostname=hostname, ssl_context=ssl_context, **config_kwargs
)
pipeline_configuration.blob_upload = True # Blob Upload is a feature on Device Clients

# Pipeline setup
http_pipeline = pipeline.HTTPPipeline(pipeline_configuration)
mqtt_pipeline = pipeline.MQTTPipeline(pipeline_configuration)

return cls(mqtt_pipeline, http_pipeline)

@classmethod
def create_from_symmetric_key(
cls, symmetric_key: str, hostname: str, device_id: str, **kwargs
Expand Down Expand Up @@ -915,6 +978,72 @@ def create_from_x509_certificate(
mqtt_pipeline = pipeline.MQTTPipeline(pipeline_configuration)
return cls(mqtt_pipeline, http_pipeline)

@classmethod
def create_from_ssl_context(
cls,
ssl_context: ssl.SSLContext | OpenSSL.SSL.Context,
hostname: str,
device_id: str,
module_id: str,
**kwargs,
) -> Self:
"""
Instantiate a client using X509 certificate authentication.

:param str hostname: Host running the IotHub.
Can be found in the Azure portal in the Overview tab as the string hostname.
:param ssl_context: Custom ssl.SSLContext
:param str device_id: The ID used to uniquely identify a device in the IoTHub
:param str module_id: The ID used to uniquely identify a module on a device on the IoTHub.

:param str server_verification_cert: Configuration Option. The trusted certificate chain.
Necessary when using connecting to an endpoint which has a non-standard root of trust,
such as a protocol gateway.
:param str gateway_hostname: Configuration Option. The gateway hostname for the gateway
device.
:param bool websockets: Configuration Option. Default is False. Set to true if using MQTT
over websockets.
:param str product_info: Configuration Option. Default is empty string. The string contains
arbitrary product info which is appended to the user agent string.
:param proxy_options: Options for sending traffic through proxy servers.
:type proxy_options: :class:`azure.iot.device.ProxyOptions`
:param int keep_alive: Maximum period in seconds between communications with the
broker. If no other messages are being exchanged, this controls the
rate at which the client will send ping messages to the broker.
If not provided default value of 60 secs will be used.
:param bool auto_connect: Automatically connect the client to IoTHub when a method is
invoked which requires a connection to be established. (Default: True)
:param bool connection_retry: Attempt to re-establish a dropped connection (Default: True)
:param int connection_retry_interval: Interval, in seconds, between attempts to
re-establish a dropped connection (Default: 10)
:param bool ensure_desired_properties: Ensure the most recent desired properties patch has
been received upon re-connections (Default:True)

:raises: TypeError if given an unsupported parameter.

:returns: An instance of an IoTHub client that uses an X509 certificate for authentication.
"""
# Ensure no invalid kwargs were passed by the user
excluded_kwargs = ["sastoken_ttl"]
_validate_kwargs(exclude=excluded_kwargs, **kwargs)

# Pipeline Config setup
config_kwargs = _get_config_kwargs(**kwargs)
pipeline_configuration = pipeline.IoTHubPipelineConfig(
device_id=device_id,
module_id=module_id,
hostname=hostname,
ssl_context=ssl_context,
**config_kwargs,
)
pipeline_configuration.blob_upload = True # Blob Upload is a feature on Device Clients

# Pipeline setup
http_pipeline = pipeline.HTTPPipeline(pipeline_configuration)
mqtt_pipeline = pipeline.MQTTPipeline(pipeline_configuration)

return cls(mqtt_pipeline, http_pipeline)

@abc.abstractmethod
def send_message_to_output(self, message: Union[Message, str], output_name: str) -> None:
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

import abc
import logging
import ssl
import OpenSSL
from typing_extensions import Self
from typing import Any, Dict, List, Optional
from azure.iot.device.provisioning import pipeline
Expand Down Expand Up @@ -95,7 +97,12 @@ def __init__(self, pipeline: MQTTPipeline):

@classmethod
def create_from_symmetric_key(
cls, provisioning_host: str, registration_id: str, id_scope: str, symmetric_key: str, **kwargs
cls,
provisioning_host: str,
registration_id: str,
id_scope: str,
symmetric_key: str,
**kwargs,
) -> Self:
"""
Create a client which can be used to run the registration of a device with provisioning service
Expand Down Expand Up @@ -159,7 +166,7 @@ def create_from_symmetric_key(
registration_id=registration_id,
id_scope=id_scope,
sastoken=sastoken,
**config_kwargs
**config_kwargs,
)

# Pipeline setup
Expand Down Expand Up @@ -221,7 +228,71 @@ def create_from_x509_certificate(
registration_id=registration_id,
id_scope=id_scope,
x509=x509,
**config_kwargs
**config_kwargs,
)

# Pipeline setup
mqtt_provisioning_pipeline = pipeline.MQTTPipeline(pipeline_configuration)

return cls(mqtt_provisioning_pipeline)

@classmethod
def create_from_ssl_context(
cls,
provisioning_host: str,
registration_id: str,
id_scope: str,
ssl_context: ssl.SSLContext | OpenSSL.SSL.Context,
**kwargs,
) -> Self:
"""
Create a client which can be used to run the registration of a device with
provisioning service using X509 certificate authentication.

:param str provisioning_host: Host running the Device Provisioning Service. Can be found in
the Azure portal in the Overview tab as the string Global device endpoint.
:param str registration_id: The registration ID used to uniquely identify a device in the
Device Provisioning Service. The registration ID is alphanumeric, lowercase string
and may contain hyphens.
:param str id_scope: The ID scope is used to uniquely identify the specific
provisioning service the device will register through. The ID scope is assigned to a
Device Provisioning Service when it is created by the user and is generated by the
service and is immutable, guaranteeing uniqueness.
:param ssl_context: Custom ssl.SSLContext

:param str server_verification_cert: Configuration Option. The trusted certificate chain.
Necessary when using connecting to an endpoint which has a non-standard root of trust,
such as a protocol gateway.
:param str gateway_hostname: Configuration Option. The gateway hostname for the gateway
device.
:param bool websockets: Configuration Option. Default is False. Set to true if using MQTT
over websockets.
:param cipher: Configuration Option. Cipher suite(s) for TLS/SSL, as a string in
"OpenSSL cipher list format" or as a list of cipher suite strings.
:type cipher: str or list(str)
:param proxy_options: Options for sending traffic through proxy servers.
:type proxy_options: :class:`azure.iot.device.ProxyOptions`
:param int keepalive: Maximum period in seconds between communications with the
broker. If no other messages are being exchanged, this controls the
rate at which the client will send ping messages to the broker.
If not provided default value of 60 secs will be used.
:raises: TypeError if given an unrecognized parameter.

:returns: A ProvisioningDeviceClient which can register via X509 client certificates.
"""
validate_registration_id(registration_id)
# Ensure no invalid kwargs were passed by the user
excluded_kwargs = ["sastoken_ttl"]
_validate_kwargs(exclude=excluded_kwargs, **kwargs)

# Pipeline Config setup
config_kwargs = _get_config_kwargs(**kwargs)
pipeline_configuration = pipeline.ProvisioningPipelineConfig(
hostname=provisioning_host,
registration_id=registration_id,
id_scope=id_scope,
ssl_context=ssl_context,
**config_kwargs,
)

# Pipeline setup
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@
"urllib3>=2.2.2,<3.0.0",
# Actual project dependencies
"deprecation>=2.1.0,<3.0.0",
"paho-mqtt>=1.6.1,<2.0.0",
"paho-mqtt @ git+https://github.com/IniterWorker/paho.mqtt.python.git@feature/pyopenssl-context",
"requests>=2.32.3,<3.0.0",
"requests-unixsocket2>=0.4.1",
"pyOpenSSL>=23.2.0",
"janus",
"PySocks",
"typing_extensions",
Expand Down
Loading