Skip to content

Commit 346f808

Browse files
authored
Merge pull request #93 from arduino/new_se_support
ussl: Add support for sign callback on MicroPython.
2 parents eae80d2 + 079922a commit 346f808

File tree

5 files changed

+224
-39
lines changed

5 files changed

+224
-39
lines changed

.github/workflows/client-test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ jobs:
5252
run: |
5353
python -m pip install --upgrade pip
5454
python -m pip install build==0.10.0 cbor2==5.4.6 M2Crypto==0.38.0 micropython-senml==0.1.0
55+
sudo apt-get update
5556
sudo apt-get install softhsm2 gnutls-bin libengine-pkcs11-openssl
5657
5758
- name: '📦 Build package'

README.md

+14-6
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,25 @@ python examples/example.py
111111
```
112112

113113
## Testing on MicroPython
114-
MicroPython currently does Not support secure elements. The username and password can be used, or the key and cert files must be stored in DER format on the filesystem. To test the client on MicroPython, first convert the key and certificate to DER, using the following commands, then copy the files to the internal storage.
115-
116-
#### Convert key and certificate to `.DER`
114+
MicroPython supports both modes of authentication: basic mode, using a username and password, and mTLS with the key and certificate stored on the filesystem or a secure element (for provisioned boards). To use key and certificate files stored on the filesystem, they must first be converted to DER format. The following commands can be used to convert from PEM to DER:
117115
```bash
118116
openssl ec -in key.pem -out key.der -outform DER
119117
openssl x509 -in cert.pem -out cert.der -outform DER
120118
```
121119

122-
#### Run the MicroPython example script
123-
* Set `KEY_PATH`, `CERT_PATH`, to key and certificate DER paths respectively.
124-
* run `examples/micropython.py`
120+
In this case `KEY_PATH`, `CERT_PATH`, can be set to the key and certificate DER paths, respectively:
121+
```Python
122+
KEY_PATH = "path/to/key.der"
123+
CERT_PATH = "path/to/cert.der"
124+
```
125+
126+
Alternatively, if the key and certificate are stored on the SE, their URIs can be specified in the following format:
127+
```Python
128+
KEY_PATH = "se05x:token=0x00000064"
129+
CERT_PATH = "se05x:token=0x00000065"
130+
```
131+
132+
With the key and certificate set, the example can be run with the following command `examples/micropython_advanced.py`
125133

126134
## Useful links
127135

examples/micropython_advanced.py

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# This file is part of the Python Arduino IoT Cloud.
2+
# Any copyright is dedicated to the Public Domain.
3+
# https://creativecommons.org/publicdomain/zero/1.0/
4+
import time
5+
import ssl # noqa
6+
import network
7+
import logging
8+
from time import strftime
9+
from arduino_iot_cloud import ArduinoCloudClient
10+
from arduino_iot_cloud import Location
11+
from arduino_iot_cloud import Schedule
12+
from arduino_iot_cloud import ColoredLight
13+
from arduino_iot_cloud import Task
14+
from arduino_iot_cloud import CADATA # noqa
15+
from random import uniform
16+
from secrets import WIFI_SSID
17+
from secrets import WIFI_PASS
18+
from secrets import DEVICE_ID
19+
20+
21+
# Provisioned boards with secure elements can provide key and
22+
# certificate URIs in the SE, in following format:
23+
KEY_PATH = "se05x:token=0x00000064" # noqa
24+
CERT_PATH = "se05x:token=0x00000065" # noqa
25+
26+
# Alternatively, the key and certificate files can be stored
27+
# on the internal filesystem in DER format:
28+
#KEY_PATH = "key.der" # noqa
29+
#CERT_PATH = "cert.der" # noqa
30+
31+
32+
def on_switch_changed(client, value):
33+
# This is a write callback for the switch that toggles the LED variable. The LED
34+
# variable can be accessed via the client object passed in the first argument.
35+
client["led"] = value
36+
37+
38+
def on_clight_changed(client, clight):
39+
logging.info(f"ColoredLight changed. Swi: {clight.swi} Bri: {clight.bri} Sat: {clight.sat} Hue: {clight.hue}")
40+
41+
42+
def user_task(client, args):
43+
# NOTE: this function should not block.
44+
# This is a user-defined task that updates the colored light. Note any registered
45+
# cloud object can be accessed using the client object passed to this function.
46+
# The composite ColoredLight object fields can be assigned to individually, using dot:
47+
client["clight"].hue = round(uniform(0, 100), 1)
48+
client["clight"].bri = round(uniform(0, 100), 1)
49+
50+
51+
def wdt_task(client, wdt):
52+
# Update the WDT to prevent it from resetting the system
53+
wdt.feed()
54+
55+
56+
def wifi_connect():
57+
if not WIFI_SSID or not WIFI_PASS:
58+
raise (Exception("Network is not configured. Set SSID and passwords in secrets.py"))
59+
wlan = network.WLAN(network.STA_IF)
60+
wlan.active(True)
61+
wlan.connect(WIFI_SSID, WIFI_PASS)
62+
while not wlan.isconnected():
63+
logging.info("Trying to connect. Note this may take a while...")
64+
time.sleep_ms(500)
65+
logging.info(f"WiFi Connected {wlan.ifconfig()}")
66+
67+
68+
if __name__ == "__main__":
69+
# Configure the logger.
70+
# All message equal or higher to the logger level are printed.
71+
# To see more debugging messages, set level=logging.DEBUG.
72+
logging.basicConfig(
73+
datefmt="%H:%M:%S",
74+
format="%(asctime)s.%(msecs)03d %(message)s",
75+
level=logging.INFO,
76+
)
77+
78+
# NOTE: Add networking code here or in boot.py
79+
wifi_connect()
80+
81+
# Create a client object to connect to the Arduino IoT cloud.
82+
# For mTLS authentication, "keyfile" and "certfile" can be paths to a DER-encoded key and
83+
# a DER-encoded certificate, or secure element (SE) URIs in the format: provider:token=slot
84+
client = ArduinoCloudClient(
85+
device_id=DEVICE_ID,
86+
ssl_params={
87+
"keyfile": KEY_PATH, "certfile": CERT_PATH, "cadata": CADATA,
88+
"verify_mode": ssl.CERT_REQUIRED, "server_hostname": "iot.arduino.cc"
89+
},
90+
sync_mode=False,
91+
)
92+
93+
# Register cloud objects.
94+
# Note: The following objects must be created first in the dashboard and linked to the device.
95+
# This cloud object is initialized with its last known value from the cloud. When this object is updated
96+
# from the dashboard, the on_switch_changed function is called with the client object and the new value.
97+
client.register("sw1", value=None, on_write=on_switch_changed, interval=0.250)
98+
99+
# This cloud object is updated manually in the switch's on_write_change callback.
100+
client.register("led", value=None)
101+
102+
# This is a periodic cloud object that gets updated at fixed intervals (in this case 1 seconed) with the
103+
# value returned from its on_read function (a formatted string of the current time). Note this object's
104+
# initial value is None, it will be initialized by calling the on_read function.
105+
client.register("clk", value=None, on_read=lambda x: strftime("%H:%M:%S", time.localtime()), interval=1.0)
106+
107+
# This is an example of a composite cloud object (a cloud object with multiple variables). In this case
108+
# a colored light with switch, hue, saturation and brightness attributes. Once initialized, the object's
109+
# attributes can be accessed using dot notation. For example: client["clight"].swi = False.
110+
client.register(ColoredLight("clight", swi=True, on_write=on_clight_changed))
111+
112+
# This is another example of a composite cloud object, a map location with lat and long attributes.
113+
client.register(Location("treasureisland", lat=31.264694, lon=29.979987))
114+
115+
# This object allows scheduling recurring events from the cloud UI. On activation of the event, if the
116+
# on_active callback is provided, it gets called with the client object and the schedule object value.
117+
# Note: The activation status of the object can also be polled using client["schedule"].active.
118+
client.register(Schedule("schedule", on_active=lambda client, value: logging.info(f"Schedule activated {value}!")))
119+
120+
# The client can also schedule user code in a task and run it along with the other cloud objects.
121+
# To schedule a user function, use the Task object and pass the task name and function in "on_run"
122+
# to client.register().
123+
client.register(Task("user_task", on_run=user_task, interval=1.0))
124+
125+
# If a Watchdog timer is available, it can be used to recover the system by resetting it, if it ever
126+
# hangs or crashes for any reason. NOTE: once the WDT is enabled it must be reset periodically to
127+
# prevent it from resetting the system, which is done in another user task.
128+
# NOTE: Change the following to True to enable the WDT.
129+
if False:
130+
try:
131+
from machine import WDT
132+
# Enable the WDT with a timeout of 5s (1s is the minimum)
133+
wdt = WDT(timeout=7500)
134+
client.register(Task("watchdog_task", on_run=wdt_task, interval=1.0, args=wdt))
135+
except (ImportError, AttributeError):
136+
pass
137+
138+
# Start the Arduino IoT cloud client. In synchronous mode, this function returns immediately
139+
# after connecting to the cloud.
140+
client.start()
141+
142+
# In sync mode, start returns after connecting, and the client must be polled periodically.
143+
while True:
144+
client.update()
145+
time.sleep(0.100)

examples/micropython_basic.py

+1-18
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@
1818
from secrets import DEVICE_ID
1919
from secrets import SECRET_KEY # noqa
2020

21-
KEY_PATH = "key.der" # noqa
22-
CERT_PATH = "cert.der" # noqa
23-
2421

2522
def on_switch_changed(client, value):
2623
# This is a write callback for the switch that toggles the LED variable. The LED
@@ -65,7 +62,7 @@ def wifi_connect():
6562
logging.basicConfig(
6663
datefmt="%H:%M:%S",
6764
format="%(asctime)s.%(msecs)03d %(message)s",
68-
level=logging.DEBUG,
65+
level=logging.INFO,
6966
)
7067

7168
# NOTE: Add networking code here or in boot.py
@@ -76,20 +73,6 @@ def wifi_connect():
7673
# ID, and the password is the secret key obtained from the IoT cloud when provisioning a device.
7774
client = ArduinoCloudClient(device_id=DEVICE_ID, username=DEVICE_ID, password=SECRET_KEY, sync_mode=False)
7875

79-
# Alternatively, the client supports key and certificate-based authentication. To use this
80-
# mode, set "keyfile" and "certfile", and specify the CA certificate (if any) in "ssl_params".
81-
# Secure elements, which can be used to store the key and certificate, are also supported.
82-
# To use secure elements, provide the key and certificate URIs (in provider:token format) and
83-
# set the token's PIN (if applicable). For example:
84-
# client = ArduinoCloudClient(
85-
# device_id=DEVICE_ID,
86-
# ssl_params={
87-
# "pin": "1234", "keyfile": KEY_PATH, "certfile": CERT_PATH, "cadata": CADATA,
88-
# "verify_mode": ssl.CERT_REQUIRED, "server_hostname" : "iot.arduino.cc"
89-
# },
90-
# sync_mode=False,
91-
# )
92-
9376
# Register cloud objects.
9477
# Note: The following objects must be created first in the dashboard and linked to the device.
9578
# This cloud object is initialized with its last known value from the cloud. When this object is updated

src/arduino_iot_cloud/ussl.py

+63-15
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,56 @@
99
import ssl
1010
import sys
1111
import logging
12-
import binascii
12+
try:
13+
from micropython import const
14+
except (ImportError, AttributeError):
15+
def const(x):
16+
return x
1317

1418
pkcs11 = None
19+
se_dev = None
1520

1621
# Default engine and provider.
1722
_ENGINE_PATH = "/usr/lib/engines-3/libpkcs11.so"
1823
_MODULE_PATH = "/usr/lib/softhsm/libsofthsm2.so"
1924

20-
# Reference EC key for NXP's PlugNTrust
21-
_EC_REF_KEY = binascii.unhexlify(
22-
b"3041020100301306072a8648ce3d020106082a8648ce3d03010704273025"
23-
b"0201010420100000000000000000000000000000000000ffffffffa5a6b5"
24-
b"b6a5a6b5b61000"
25+
# Reference EC key for the SE.
26+
_EC_REF_KEY = const(
27+
b"\x30\x41\x02\x01\x00\x30\x13\x06\x07\x2A\x86\x48\xCE\x3D\x02\x01"
28+
b"\x06\x08\x2A\x86\x48\xCE\x3D\x03\x01\x07\x04\x27\x30\x25\x02\x01"
29+
b"\x01\x04\x20\xA5\xA6\xB5\xB6\xA5\xA6\xB5\xB6\x00\x00\x00\x00\x00"
30+
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF"
31+
b"\xFF\xFF\xFF"
2532
)
2633

2734

35+
def log_level_enabled(level):
36+
return logging.getLogger().isEnabledFor(level)
37+
38+
39+
def ecdsa_sign_callback(key, data):
40+
if log_level_enabled(logging.DEBUG):
41+
key_hex = "".join("%02X" % b for b in key)
42+
logging.debug(f"ecdsa_sign_callback key:{key_hex}")
43+
44+
if key[0:8] != b"\xA5\xA6\xB5\xB6\xA5\xA6\xB5\xB6":
45+
if log_level_enabled(logging.DEBUG):
46+
logging.debug("ecdsa_sign_callback falling back to default sign")
47+
return None
48+
49+
obj_id = int.from_bytes(key[-4:], "big")
50+
if log_level_enabled(logging.DEBUG):
51+
logging.debug(f"ecdsa_sign_callback oid: 0x{obj_id:02X}")
52+
53+
# Sign data on SE using reference key object id.
54+
sig = se_dev.sign(obj_id, data)
55+
if log_level_enabled(logging.DEBUG):
56+
sig_hex = "".join("%02X" % b for b in sig)
57+
logging.debug(f"ecdsa_sign_callback sig: {sig_hex}")
58+
logging.info("Signed using secure element")
59+
return sig
60+
61+
2862
def wrap_socket(sock, ssl_params={}):
2963
keyfile = ssl_params.get("keyfile", None)
3064
certfile = ssl_params.get("certfile", None)
@@ -33,16 +67,27 @@ def wrap_socket(sock, ssl_params={}):
3367
ciphers = ssl_params.get("ciphers", None)
3468
verify = ssl_params.get("verify_mode", ssl.CERT_NONE)
3569
hostname = ssl_params.get("server_hostname", None)
36-
micropython = sys.implementation.name == "micropython"
3770

38-
if keyfile is not None and "token" in keyfile and micropython:
39-
# Create a reference EC key for NXP EdgeLock device.
40-
objid = int(keyfile.split("=")[1], 16).to_bytes(4, "big")
41-
keyfile = _EC_REF_KEY[0:53] + objid + _EC_REF_KEY[57:]
42-
# Load the certificate from the secure element (when supported).
43-
# import cryptoki
44-
# with cryptoki.open() as token:
45-
# cert = token.read(0x65, 412)
71+
se_key_token = keyfile is not None and "token" in keyfile
72+
se_crt_token = certfile is not None and "token" in certfile
73+
sys_micropython = sys.implementation.name == "micropython"
74+
75+
if sys_micropython and (se_key_token or se_crt_token):
76+
import se05x
77+
78+
# Create and initialize SE05x device.
79+
global se_dev
80+
if se_dev is None:
81+
se_dev = se05x.SE05X()
82+
83+
if se_key_token:
84+
# Create a reference key for the secure element.
85+
obj_id = int(keyfile.split("=")[1], 16)
86+
keyfile = _EC_REF_KEY[0:-4] + obj_id.to_bytes(4, "big")
87+
88+
if se_crt_token:
89+
# Load the certificate from the secure element.
90+
certfile = se_dev.read(0x65, 412)
4691

4792
if keyfile is None or "token" not in keyfile:
4893
# Use MicroPython/CPython SSL to wrap socket.
@@ -58,6 +103,9 @@ def wrap_socket(sock, ssl_params={}):
58103
ctx.set_ciphers(ciphers)
59104
if cafile is not None or cadata is not None:
60105
ctx.load_verify_locations(cafile=cafile, cadata=cadata)
106+
if sys_micropython and se_key_token:
107+
# Set alternate ECDSA sign function.
108+
ctx._context.ecdsa_sign_callback = ecdsa_sign_callback
61109
return ctx.wrap_socket(sock, server_hostname=hostname)
62110
else:
63111
# Use M2Crypto to load key and cert from HSM.

0 commit comments

Comments
 (0)