Skip to content

Commit c32228e

Browse files
authored
Merge pull request #66 from arduino/m2crypto_fix
ussl: Clean up SSL and m2crypto code.
2 parents 9ac4199 + 7b315b6 commit c32228e

File tree

9 files changed

+106
-77
lines changed

9 files changed

+106
-77
lines changed

.github/workflows/client-test.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,15 @@ jobs:
7676
DEVICE_ID: ${{ secrets.DEVICE_ID1 }}
7777
SECRET_KEY: ${{ secrets.SECRET_KEY }}
7878
run: |
79-
python tests/ci.py
79+
python tests/ci.py --basic-auth
80+
81+
- name: '☁️ Connect to IoT cloud (CPython / Key/Cert Auth)'
82+
env:
83+
DEVICE_ID: ${{ secrets.DEVICE_ID2 }}
84+
SECRET_KEY: ${{ secrets.SECRET_KEY }}
85+
run: |
86+
python tests/ci.py --file-auth
87+
8088
8189
- name: '☁️ Connect to IoT cloud (CPython / Crypto Auth)'
8290
env:
@@ -93,4 +101,4 @@ jobs:
93101
run: |
94102
export PATH="${HOME}/cache/bin:${PATH}"
95103
micropython -c "import sys; print(sys.path)"
96-
micropython tests/ci.py
104+
micropython tests/ci.py --basic-auth

examples/example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from arduino_iot_cloud import Task
1212
from random import uniform
1313
import argparse
14-
import arduino_iot_cloud.ussl as ssl
14+
import ssl
1515

1616
from secrets import DEVICE_ID
1717
from secrets import SECRET_KEY # noqa

examples/micropython.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Any copyright is dedicated to the Public Domain.
33
# https://creativecommons.org/publicdomain/zero/1.0/
44
import time
5-
import ussl
5+
import ssl
66
import network
77
import logging
88
from time import strftime
@@ -73,7 +73,7 @@ def wifi_connect():
7373
client = ArduinoCloudClient(
7474
device_id=DEVICE_ID,
7575
ssl_params={
76-
"keyfile": KEY_PATH, "certfile": CERT_PATH, "cadata": CADATA, "cert_reqs": ussl.CERT_REQUIRED
76+
"keyfile": KEY_PATH, "certfile": CERT_PATH, "cadata": CADATA, "cert_reqs": ssl.CERT_REQUIRED
7777
}
7878
)
7979

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ classifiers = [
2222
]
2323
dependencies = [
2424
'cbor2 >= 5.4.6',
25-
'M2Crypto >= 0.38.0',
2625
'micropython-senml >= 0.1.0',
2726
]
2827

src/arduino_iot_cloud/ucloud.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
66

77
import time
8+
import sys
89
import logging
910
from senml import SenmlPack
1011
from senml import SenmlRecord
@@ -175,6 +176,14 @@ def __init__(
175176
self.senmlpack = SenmlPack("", self.senml_generic_callback)
176177
self.started = False
177178

179+
if "pin" in ssl_params:
180+
try:
181+
# Use M2Crypto to load key and cert from HSM.
182+
import M2Crypto # noqa
183+
except (ImportError, AttributeError):
184+
logging.error("The m2crypto module is required to use HSM.")
185+
sys.exit(1)
186+
178187
# Convert args to bytes if they are passed as strings.
179188
if isinstance(device_id, str):
180189
device_id = bytes(device_id, "utf-8")
@@ -188,19 +197,6 @@ def __init__(
188197
# Update RTC from NTP server on MicroPython.
189198
self.update_systime(ntp_server, ntp_timeout)
190199

191-
# MicroPython does not support secure elements yet, and key/cert
192-
# must be loaded from DER files and passed as binary blobs.
193-
if "keyfile" in ssl_params and "der" in ssl_params["keyfile"]:
194-
with open(ssl_params.pop("keyfile"), "rb") as f:
195-
ssl_params["key"] = f.read()
196-
if "certfile" in ssl_params and "der" in ssl_params["certfile"]:
197-
with open(ssl_params.pop("certfile"), "rb") as f:
198-
ssl_params["cert"] = f.read()
199-
200-
if "ca_certs" in ssl_params and "der" in ssl_params["ca_certs"]:
201-
with open(ssl_params.pop("ca_certs"), "rb") as f:
202-
ssl_params["cadata"] = f.read()
203-
204200
# If no server/port were passed in args, set the default server/port
205201
# based on authentication type.
206202
if server is None:

src/arduino_iot_cloud/umqtt.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@
2626
import struct
2727
import select
2828
import logging
29-
30-
try:
31-
from ussl import wrap_socket
32-
except ImportError:
33-
from arduino_iot_cloud.ussl import wrap_socket
29+
import arduino_iot_cloud.ussl as ssl
3430

3531

3632
class MQTTException(Exception):
@@ -99,14 +95,14 @@ def connect(self, clean_session=True, timeout=5.0):
9995
try:
10096
self.sock = socket.socket()
10197
self.sock.settimeout(timeout)
102-
self.sock = wrap_socket(self.sock, **self.ssl_params)
98+
self.sock = ssl.wrap_socket(self.sock, self.ssl_params)
10399
self.sock.connect(addr)
104100
except Exception:
105101
self.sock.close()
106102
self.sock = socket.socket()
107103
self.sock.settimeout(timeout)
108104
self.sock.connect(addr)
109-
self.sock = wrap_socket(self.sock, **self.ssl_params)
105+
self.sock = ssl.wrap_socket(self.sock, self.ssl_params)
110106

111107
premsg = bytearray(b"\x10\0\0\0\0\0")
112108
msg = bytearray(b"\x04MQTT\x04\x02\0\0")

src/arduino_iot_cloud/ussl.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,64 +6,72 @@
66
#
77
# SSL module with m2crypto backend for HSM support.
88

9-
from M2Crypto import Engine, m2, SSL
9+
import sys
10+
import ssl
1011

11-
CERT_NONE = SSL.verify_none
12-
CERT_REQUIRED = SSL.verify_peer
13-
14-
_key = None
15-
_cert = None
12+
pkcs11 = None
1613

1714
# Default engine and provider.
1815
_ENGINE_PATH = "/usr/lib/engines-3/libpkcs11.so"
1916
_MODULE_PATH = "/usr/lib/softhsm/libsofthsm2.so"
2017

2118

22-
def init(pin, certfile, keyfile, engine_path, module_path):
23-
global _key, _cert
24-
Engine.load_dynamic_engine("pkcs11", engine_path)
25-
pkcs11 = Engine.Engine("pkcs11")
26-
pkcs11.ctrl_cmd_string("MODULE_PATH", module_path)
27-
pkcs11.ctrl_cmd_string("PIN", pin)
28-
pkcs11.init()
29-
_key = pkcs11.load_private_key(keyfile)
30-
_cert = pkcs11.load_certificate(certfile)
31-
32-
3319
def wrap_socket(
34-
sock_in,
35-
pin=None,
36-
certfile=None,
37-
keyfile=None,
38-
ca_certs=None,
39-
cert_reqs=CERT_NONE,
40-
ciphers=None,
41-
engine_path=_ENGINE_PATH,
42-
module_path=_MODULE_PATH,
20+
sock,
21+
ssl_params={},
4322
):
44-
if certfile is None or keyfile is None:
45-
# Fallback to Python's SSL
46-
import ssl
47-
return ssl.wrap_socket(sock_in)
23+
if any(k not in ssl_params for k in ("keyfile", "certfile", "pin")):
24+
# Use Micro/CPython's SSL
25+
if sys.implementation.name == "micropython":
26+
# Load key, cert and CA from DER files, and pass them as binary blobs.
27+
mpargs = {"keyfile": "key", "certfile": "cert", "ca_certs": "cadata"}
28+
for k, v in mpargs.items():
29+
if k in ssl_params and "der" in ssl_params[k]:
30+
with open(ssl_params.pop(k), "rb") as f:
31+
ssl_params[v] = f.read()
32+
return ssl.wrap_socket(sock, **ssl_params)
33+
34+
# Use M2Crypto to load key and cert from HSM.
35+
from M2Crypto import m2, SSL, Engine
4836

49-
if _key is None or _cert is None:
50-
init(pin, certfile, keyfile, engine_path, module_path)
37+
global pkcs11
38+
if pkcs11 is None:
39+
pkcs11 = Engine.load_dynamic_engine(
40+
"pkcs11", ssl_params.get("engine_path", _ENGINE_PATH)
41+
)
42+
pkcs11.ctrl_cmd_string(
43+
"MODULE_PATH", ssl_params.get("module_path", _MODULE_PATH)
44+
)
45+
pkcs11.ctrl_cmd_string("PIN", ssl_params["pin"])
46+
pkcs11.init()
5147

52-
# Create SSL context
48+
# Create and configure SSL context
5349
ctx = SSL.Context("tls")
5450
ctx.set_default_verify_paths()
5551
ctx.set_allow_unknown_ca(False)
5652

53+
ciphers = ssl_params.get("ciphers", None)
5754
if ciphers is not None:
5855
ctx.set_cipher_list(ciphers)
5956

60-
if ca_certs is not None and cert_reqs is not CERT_NONE:
57+
ca_certs = ssl_params.get("ca_certs", None)
58+
if ca_certs is not None:
6159
if ctx.load_verify_locations(ca_certs) != 1:
6260
raise Exception("Failed to load CA certs")
63-
ctx.set_verify(SSL.verify_peer, depth=9)
61+
62+
cert_reqs = ssl_params.get("cert_reqs", ssl.CERT_NONE)
63+
if cert_reqs == ssl.CERT_NONE:
64+
cert_reqs = SSL.verify_none
65+
else:
66+
cert_reqs = SSL.verify_peer
67+
ctx.set_verify(cert_reqs, depth=9)
6468

6569
# Set key/cert
66-
m2.ssl_ctx_use_x509(ctx.ctx, _cert.x509)
67-
m2.ssl_ctx_use_pkey_privkey(ctx.ctx, _key.pkey)
70+
key = pkcs11.load_private_key(ssl_params["keyfile"])
71+
m2.ssl_ctx_use_pkey_privkey(ctx.ctx, key.pkey)
72+
73+
cert = pkcs11.load_certificate(ssl_params["certfile"])
74+
m2.ssl_ctx_use_x509(ctx.ctx, cert.x509)
75+
6876
SSL.Connection.postConnectionCheck = None
69-
return SSL.Connection(ctx, sock=sock_in)
77+
return SSL.Connection(ctx, sock=sock)

tests/ci.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ def on_value_changed(client, value):
2727
"-d", "--debug", action="store_true", help="Enable debugging messages"
2828
)
2929
parser.add_argument(
30-
"-c", "--crypto-device", action="store_true", help="Use crypto device"
30+
"-b", "--basic-auth", action="store_true", help="Username and password auth",
31+
)
32+
parser.add_argument(
33+
"-c", "--crypto-device", action="store_true", help="Use soft-hsm/crypto device",
34+
)
35+
parser.add_argument(
36+
"-f", "--file-auth", action="store_true", help="Use key/cert files"
3137
)
3238
args = parser.parse_args()
3339

@@ -45,8 +51,25 @@ def on_value_changed(client, value):
4551
# the CA certificate (if any) in "ssl_params". Alternatively, a username and password can
4652
# be used to authenticate, for example:
4753
# client = ArduinoCloudClient(device_id=DEVICE_ID, username=DEVICE_ID, password=SECRET_KEY)
48-
if args.crypto_device:
49-
import arduino_iot_cloud.ussl as ssl
54+
if args.basic_auth:
55+
client = ArduinoCloudClient(
56+
device_id=os.getenv("DEVICE_ID"),
57+
username=os.getenv("DEVICE_ID"),
58+
password=os.getenv("SECRET_KEY"),
59+
)
60+
elif args.file_auth:
61+
import ssl
62+
client = ArduinoCloudClient(
63+
device_id=os.getenv("DEVICE_ID"),
64+
ssl_params={
65+
"keyfile": "key.pem",
66+
"certfile": "cert.pem",
67+
"ca_certs": "ca-root.pem",
68+
"cert_reqs": ssl.CERT_REQUIRED,
69+
},
70+
)
71+
elif args.crypto_device:
72+
import ssl
5073
client = ArduinoCloudClient(
5174
device_id=os.getenv("DEVICE_ID"),
5275
ssl_params={
@@ -60,11 +83,8 @@ def on_value_changed(client, value):
6083
},
6184
)
6285
else:
63-
client = ArduinoCloudClient(
64-
device_id=os.getenv("DEVICE_ID"),
65-
username=os.getenv("DEVICE_ID"),
66-
password=os.getenv("SECRET_KEY"),
67-
)
86+
parser.print_help()
87+
sys.exit(1)
6888

6989
# Register cloud objects.
7090
# Note: The following objects must be created first in the dashboard and linked to the device.

tests/ci.sh

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,19 @@ ci_install_micropython() {
99
git clone --depth=1 https://github.com/micropython/micropython.git
1010

1111
cat > micropython/ports/unix/manifest.py <<-EOF
12-
include("\$(PORT_DIR)/variants/manifest.py")
13-
include("\$(MPY_DIR)/extmod/asyncio")
12+
include("\$(PORT_DIR)/variants/standard/manifest.py")
1413
require("bundle-networking")
1514
require("time")
1615
require("senml")
1716
require("logging")
1817
EOF
1918

20-
make -C micropython/mpy-cross/
21-
make -C micropython/ports/unix/ submodules
22-
make -C micropython/ports/unix/ FROZEN_MANIFEST=manifest.py CFLAGS_EXTRA="-DMICROPY_PY_SELECT=1"
19+
echo "#undef MICROPY_PY_SELECT_SELECT" >> micropython/ports/unix/variants/mpconfigvariant_common.h
20+
echo "#undef MICROPY_PY_SELECT_POSIX_OPTIMISATIONS" >> micropython/ports/unix/variants/mpconfigvariant_common.h
21+
22+
make -j12 -C micropython/mpy-cross/
23+
make -j12 -C micropython/ports/unix/ submodules
24+
make -j12 -C micropython/ports/unix/ FROZEN_MANIFEST=manifest.py CFLAGS_EXTRA="-DMICROPY_PY_SELECT=1"
2325
cp micropython/ports/unix/build-standard/micropython ${CACHE_DIR}
2426
}
2527

0 commit comments

Comments
 (0)