Skip to content

Commit

Permalink
module id for x509 (#129)
Browse files Browse the repository at this point in the history
module id for x509
  • Loading branch information
olivakar authored Jul 13, 2019
1 parent 63e305e commit c001a55
Show file tree
Hide file tree
Showing 9 changed files with 309 additions and 83 deletions.
24 changes: 21 additions & 3 deletions azure-iot-device/azure/iot/device/iothub/abstract_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,18 @@ def receive_twin_desired_properties_patch(self):
@six.add_metaclass(abc.ABCMeta)
class AbstractIoTHubDeviceClient(AbstractIoTHubClient):
@classmethod
def create_from_x509_certificate(cls, hostname, device_id, x509):
def create_from_x509_certificate(cls, x509, hostname, device_id):
"""
Instantiate a client which using X509 certificate authentication.
:param hostname: Host running the IotHub. Can be found in the Azure portal in the Overview tab as the string hostname.
:param device_id: The ID is used to uniquely identify a device in the IoTHub
:param x509: The complete x509 certificate object, To use the certificate the enrollment object needs to contain cert (either the root certificate or one of the intermediate CA certificates).
If the cert comes from a CER file, it needs to be base64 encoded.
:type x509: X509
:param device_id: The ID is used to uniquely identify a device in the IoTHub
:return: A IoTHubClient which can use X509 authentication.
"""
authentication_provider = auth.X509AuthenticationProvider(
hostname=hostname, device_id=device_id, x509=x509
x509=x509, hostname=hostname, device_id=device_id
)
pipeline = IoTHubPipeline(authentication_provider)
return cls(pipeline)
Expand Down Expand Up @@ -190,6 +190,24 @@ def create_from_edge_environment(cls):
pipeline = IoTHubPipeline(authentication_provider)
return cls(pipeline)

@classmethod
def create_from_x509_certificate(cls, x509, hostname, device_id, module_id):
"""
Instantiate a client which using X509 certificate authentication.
:param hostname: Host running the IotHub. Can be found in the Azure portal in the Overview tab as the string hostname.
:param x509: The complete x509 certificate object, To use the certificate the enrollment object needs to contain cert (either the root certificate or one of the intermediate CA certificates).
If the cert comes from a CER file, it needs to be base64 encoded.
:type x509: X509
:param device_id: The ID is used to uniquely identify a device in the IoTHub
:param module_id : The ID of the module to uniquely identify a module on a device on the IoTHub.
:return: A IoTHubClient which can use X509 authentication.
"""
authentication_provider = auth.X509AuthenticationProvider(
x509=x509, hostname=hostname, device_id=device_id, module_id=module_id
)
pipeline = IoTHubPipeline(authentication_provider)
return cls(pipeline)

@abc.abstractmethod
def send_to_output(self, message, output_name):
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ class X509AuthenticationProvider(AuthenticationProvider):
for device identities connecting directly to an Azure IoT hub.
"""

def __init__(self, hostname, device_id, x509):
def __init__(self, x509, hostname, device_id, module_id=None):
"""
Constructor for X509 Authentication Provider
:param x509: The X509 object containing certificate, key and passphrase
:param hostname: The hostname of the Azure IoT hub.
:param device_id: The device unique identifier as it exists in the Azure IoT Hub device registry.
:param x509: The X509 object containing certificate, key and passphrase
:param module_id: The module unique identifier of the device. It is not applicable when dealing with only devices.
"""
logger.info("Using X509 authentication for {%s, %s}", hostname, device_id)
super(X509AuthenticationProvider, self).__init__(hostname=hostname, device_id=device_id)
logger.info(
"Using X509 authentication for {hostname},{device_id},{module_id}".format(
hostname=hostname, device_id=device_id, module_id=module_id
)
)
super(X509AuthenticationProvider, self).__init__(
hostname=hostname, device_id=device_id, module_id=module_id
)
self._x509 = x509

def get_x509_certificate(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------

import os
import uuid
from azure.iot.device.aio import IoTHubModuleClient
from azure.iot.device.iothub import Message
from azure.iot.device.common import X509
import logging
import asyncio


logging.basicConfig(level=logging.DEBUG)

messages_to_send = 10


async def main():
hostname = os.getenv("HOSTNAME")

# The device having a certain module that has been created on the portal
# using X509 CA signing or Self signing capabilities

device_id = os.getenv("DEVICE_ID")
module_id = os.getenv("MODULE_ID")

x509 = X509(
cert_file=os.getenv("X509_CERT_FILE"),
key_file=os.getenv("X509_KEY_FILE"),
pass_phrase=os.getenv("PASS_PHRASE"),
)

module_client = IoTHubModuleClient.create_from_x509_certificate(
hostname=hostname, x509=x509, device_id=device_id, module_id=module_id
)

# Connect the client.
await module_client.connect()

async def send_test_message(i):
print("sending message #" + str(i))
msg = Message("test wind speed " + str(i))
msg.message_id = uuid.uuid4()
msg.correlation_id = "correlation-1234"
msg.custom_properties["tornado-warning"] = "yes"
await module_client.send_d2c_message(msg)
print("done sending message #" + str(i))

await asyncio.gather(*[send_test_message(i) for i in range(1, messages_to_send + 1)])

# finally, disconnect
await module_client.disconnect()


if __name__ == "__main__":
asyncio.run(main())

# If using Python 3.6 or below, use the following code instead of asyncio.run(main()):
# loop = asyncio.get_event_loop()
# loop.run_until_complete(main())
# loop.close()
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for
# license information.
# --------------------------------------------------------------------------

import os
import time
import uuid
from azure.iot.device import IoTHubModuleClient, Message
from azure.iot.device.common import X509
import logging


logging.basicConfig(level=logging.ERROR)

hostname = os.getenv("HOSTNAME")

# The device having a certain module that has been created on the portal
# using X509 CA signing or Self signing capabilities
# The <device_id>\<module_id> should be the common name of the certificate

device_id = os.getenv("DEVICE_ID")
module_id = os.getenv("MODULE_ID")

x509 = X509(
cert_file=os.getenv("X509_CERT_FILE"),
key_file=os.getenv("X509_KEY_FILE"),
pass_phrase=os.getenv("PASS_PHRASE"),
)

module_client = IoTHubModuleClient.create_from_x509_certificate(
hostname=hostname, x509=x509, device_id=device_id, module_id=module_id
)

module_client.connect()


# send 5 messages with a 1 second pause between each message
for i in range(1, 6):
print("sending message #" + str(i))
msg = Message("test wind speed " + str(i))
msg.message_id = uuid.uuid4()
msg.correlation_id = "correlation-1234"
msg.custom_properties["tornado-warning"] = "yes"
module_client.send_d2c_message(msg)
time.sleep(1)

# send only string messages
for i in range(6, 11):
print("sending message #" + str(i))
module_client.send_d2c_message("test payload message " + str(i))
time.sleep(1)


# finally, disconnect
module_client.disconnect()
78 changes: 51 additions & 27 deletions azure-iot-device/tests/iothub/aio/test_async_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,29 +154,6 @@ async def test_raises_value_error_on_bad_sas_token(self, client_class, bad_sas):
client_class.create_from_shared_access_signature(bad_sas)


class SharedClientFromCreateFromX509Certificate(object):
@pytest.mark.it("Instantiates the client, given a valid X509 certificate object")
async def test_instantiates_client(self, client_class, x509):
client = client_class.create_from_x509_certificate(
hostname="durmstranginstitute.farend", device_id="MySnitch", x509=x509
)
assert isinstance(client, client_class)

@pytest.mark.it("Uses a X509AuthenticationProvider to create the client's IoTHub pipeline")
async def test_auth_provider_and_pipeline(self, mocker, client_class):
mock_auth = mocker.patch("azure.iot.device.iothub.auth.X509AuthenticationProvider")
mock_pipeline_init = mocker.patch("azure.iot.device.iothub.abstract_clients.IoTHubPipeline")

client = client_class.create_from_x509_certificate(
hostname="durmstranginstitute.farend", device_id="MySnitch", x509=mocker.MagicMock()
)

assert mock_auth.call_count == 1
assert mock_pipeline_init.call_count == 1
assert mock_pipeline_init.call_args == mocker.call(mock_auth.return_value)
assert client._pipeline == mock_pipeline_init.return_value


class SharedClientConnectTests(object):
@pytest.mark.it("Begins a 'connect' pipeline operation")
async def test_calls_pipeline_connect(self, client, pipeline):
Expand Down Expand Up @@ -571,10 +548,27 @@ class TestIoTHubDeviceClientCreateFromSharedAccessSignature(


@pytest.mark.describe("IoTHubDeviceClient (Asynchronous) - .create_from_x509_certificate()")
class TestIoTHubDeviceClientCreateFromX509Certificate(
IoTHubDeviceClientTestsConfig, SharedClientFromCreateFromX509Certificate
):
pass
class TestIoTHubDeviceClientCreateFromX509Certificate(IoTHubDeviceClientTestsConfig):
@pytest.mark.it("Instantiates the client, given a valid X509 certificate object")
async def test_instantiates_client(self, client_class, x509):
client = client_class.create_from_x509_certificate(
hostname="durmstranginstitute.farend", x509=x509, device_id="MySnitch"
)
assert isinstance(client, client_class)

@pytest.mark.it("Uses an X509AuthenticationProvider to create the client's IoTHub pipeline")
async def test_auth_provider_and_pipeline(self, mocker, client_class):
mock_auth = mocker.patch("azure.iot.device.iothub.auth.X509AuthenticationProvider")
mock_pipeline_init = mocker.patch("azure.iot.device.iothub.abstract_clients.IoTHubPipeline")

client = client_class.create_from_x509_certificate(
hostname="durmstranginstitute.farend", x509=mocker.MagicMock(), device_id="MySnitch"
)

assert mock_auth.call_count == 1
assert mock_pipeline_init.call_count == 1
assert mock_pipeline_init.call_args == mocker.call(mock_auth.return_value)
assert client._pipeline == mock_pipeline_init.return_value


@pytest.mark.describe("IoTHubDeviceClient (Asynchronous) - .connect()")
Expand Down Expand Up @@ -949,6 +943,36 @@ async def test_bad_file_io(self, mocker, client_class, edge_local_debug_environm
client_class.create_from_edge_environment()


@pytest.mark.describe("IoTHubModuleClient (Asynchronous) - .create_from_x509_certificate()")
class TestIoTHubModuleClientCreateFromX509Certificate(IoTHubModuleClientTestsConfig):
@pytest.mark.it("Instantiates the client, given a valid X509 certificate object")
async def test_instantiates_client(self, client_class, x509):
client = client_class.create_from_x509_certificate(
hostname="durmstranginstitute.farend",
x509=x509,
device_id="MySnitch",
module_id="Charms",
)
assert isinstance(client, client_class)

@pytest.mark.it("Uses an X509AuthenticationProvider to create the client's IoTHub pipeline")
async def test_auth_provider_and_pipeline(self, mocker, client_class):
mock_auth = mocker.patch("azure.iot.device.iothub.auth.X509AuthenticationProvider")
mock_pipeline_init = mocker.patch("azure.iot.device.iothub.abstract_clients.IoTHubPipeline")

client = client_class.create_from_x509_certificate(
hostname="durmstranginstitute.farend",
x509=mocker.MagicMock(),
device_id="MySnitch",
module_id="Charms",
)

assert mock_auth.call_count == 1
assert mock_pipeline_init.call_count == 1
assert mock_pipeline_init.call_args == mocker.call(mock_auth.return_value)
assert client._pipeline == mock_pipeline_init.return_value


@pytest.mark.describe("IoTHubModuleClient (Asynchronous) - .connect()")
class TestIoTHubModuleClientConnect(IoTHubModuleClientTestsConfig, SharedClientConnectTests):
pass
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
fake_x509_cert_value = "fantastic_beasts"
fake_x509_cert_key = "where_to_find_them"
fake_pass_phrase = "alohomora"
module_id = "Transfiguration"


def x509():
Expand All @@ -21,26 +22,42 @@ def x509():

@pytest.mark.describe("X509AuthenticationProvider")
class TestX509AuthenticationProvider(object):
@pytest.mark.it("Instantiates correctly with hostname")
@pytest.mark.it("Instantiates with hostname")
def test_instantiates_correctly_with_hostname(self):
x509_cert_object = x509()
x509_auth_provider = X509AuthenticationProvider(
hostname=hostname, device_id=device_id, x509=x509_cert_object
x509=x509_cert_object, hostname=hostname, device_id=device_id
)
assert x509_auth_provider.hostname == hostname

@pytest.mark.it("Instantiates correctly with device_id")
@pytest.mark.it("Instantiates with device_id")
def test_instantiates_correctly_with_device_id(self):
x509_cert_object = x509()
x509_auth_provider = X509AuthenticationProvider(
hostname=hostname, device_id=device_id, x509=x509_cert_object
x509=x509_cert_object, hostname=hostname, device_id=device_id
)
assert x509_auth_provider.device_id == device_id

@pytest.mark.it("Getter correctly retrieves the x509 certificate object")
@pytest.mark.it("Instantiates with module_id")
def test_instantiates_correctly_with_module_id(self):
x509_cert_object = x509()
x509_auth_provider = X509AuthenticationProvider(
x509=x509_cert_object, hostname=hostname, device_id=device_id, module_id=module_id
)
assert x509_auth_provider.module_id == module_id

@pytest.mark.it("Instantiates with module_id defaulting to None")
def test_instantiates_correctly_with_device_id_and_optional_module_id(self):
x509_cert_object = x509()
x509_auth_provider = X509AuthenticationProvider(
x509=x509_cert_object, hostname=hostname, device_id=device_id
)
assert x509_auth_provider.module_id is None

@pytest.mark.it("Getter retrieves the x509 certificate object")
def test_get_certificate(self):
x509_cert_object = x509()
x509_auth_provider = X509AuthenticationProvider(
hostname=hostname, device_id=device_id, x509=x509_cert_object
x509=x509_cert_object, hostname=hostname, device_id=device_id
)
assert x509_auth_provider.get_x509_certificate()
Loading

0 comments on commit c001a55

Please sign in to comment.