Skip to content

Commit c7e6bfe

Browse files
committed
Merge moduasyncio: Add SSL support micropython#5840
2 parents 843c956 + f68a1ca commit c7e6bfe

File tree

8 files changed

+343
-13
lines changed

8 files changed

+343
-13
lines changed

docs/library/ussl.rst

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,24 @@ facilities for network sockets, both client-side and server-side.
1313
Functions
1414
---------
1515

16-
.. function:: ussl.wrap_socket(sock, server_side=False, keyfile=None, certfile=None, cert_reqs=CERT_NONE, ca_certs=None, do_handshake=True)
16+
.. function:: ussl.wrap_socket(sock, server_side=False, keyfile=None, certfile=None, cert_reqs=CERT_NONE, ca_certs=None, server_hostname=None, do_handshake=True)
17+
1718
Takes a `stream` *sock* (usually usocket.socket instance of ``SOCK_STREAM`` type),
1819
and returns an instance of ssl.SSLSocket, which wraps the underlying stream in
19-
an SSL context. Returned object has the usual `stream` interface methods like
20+
an SSL context. The returned object has the usual `stream` interface methods like
2021
``read()``, ``write()``, etc.
2122
A server-side SSL socket should be created from a normal socket returned from
2223
:meth:`~usocket.socket.accept()` on a non-SSL listening server socket.
2324

24-
- *do_handshake* determines whether the handshake is done as part of the ``wrap_socket``
25+
Parameters:
26+
27+
- ``server_side``: creates a server connection if True, else client connection. A
28+
server connection requires a ``keyfile`` and a ``certfile``.
29+
- ``cert_reqs``: specifies the level of certificate checking to be performed.
30+
- ``ca_certs``: root certificates to use for certificate checking.
31+
- ``server_hostname``: specifies the hostname of the server for verification purposes
32+
as well for SNI (Server Name Identification).
33+
- ``do_handshake``: determines whether the handshake is done as part of the ``wrap_socket``
2534
or whether it is deferred to be done as part of the initial reads or writes
2635
(there is no ``do_handshake`` method as in CPython).
2736
For blocking sockets doing the handshake immediately is standard. For non-blocking
@@ -58,3 +67,11 @@ Constants
5867
ussl.CERT_REQUIRED
5968

6069
Supported values for *cert_reqs* parameter.
70+
71+
- CERT_NONE: in client mode accept just about any cert, in server mode do not
72+
request a cert from the client.
73+
- CERT_OPTIONAL: in client mode behaves the same as CERT_REQUIRED and in server
74+
mode requests an optional cert from the client for authentication.
75+
- CERT_REQUIRED: in client mode validates the server's cert and
76+
in server mode requires the client to send a cert for authentication. Note that
77+
ussl does not actually support client authentication.

extmod/modussl_mbedtls.c

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@
4646
#include "mbedtls/debug.h"
4747
#include "mbedtls/error.h"
4848

49+
// flags for _mp_obj_ssl_socket_t.poll_flag that control the poll ioctl
50+
// the issue is that when using ipoll we may be polling only for reading, and the socket may never
51+
// become readable because mbedtls needs to write soemthing (like a handshake or renegotiation) and
52+
// so poll never returns "it's readable" or "it's writable" and so nothing ever makes progress.
53+
// See also the commit message for
54+
// https://github.com/micropython/micropython/commit/9c7c082396f717a8a8eb845a0af407e78d38165f
55+
#define READ_NEEDS_WRITE 0x1 // mbedtls_ssl_read said "I need a write"
56+
#define WRITE_NEEDS_READ 0x2 // mbedtls_ssl_write said "I need a read"
57+
4958
typedef struct _mp_obj_ssl_socket_t {
5059
mp_obj_base_t base;
5160
mp_obj_t sock;
@@ -56,6 +65,8 @@ typedef struct _mp_obj_ssl_socket_t {
5665
mbedtls_x509_crt cacert;
5766
mbedtls_x509_crt cert;
5867
mbedtls_pk_context pkey;
68+
uint8_t poll_flag;
69+
uint8_t poll_by_read; // true: at next poll try to read first
5970
} mp_obj_ssl_socket_t;
6071

6172
struct ssl_args {
@@ -116,6 +127,27 @@ STATIC NORETURN void mbedtls_raise_error(int err) {
116127
#endif
117128
}
118129

130+
STATIC mp_obj_t mod_ssl_errstr(mp_obj_t err_in) {
131+
size_t err = mp_obj_get_int(err_in);
132+
vstr_t vstr;
133+
vstr_init_len(&vstr, 80);
134+
135+
// Including mbedtls_strerror takes about 16KB on the esp32 due to all the strings
136+
#if 1
137+
vstr.buf[0] = 0;
138+
mbedtls_strerror(err, vstr.buf, vstr.alloc);
139+
vstr.len = strlen(vstr.buf);
140+
if (vstr.len == 0) {
141+
return MP_OBJ_NULL;
142+
}
143+
#else
144+
vstr_printf(vstr, "mbedtls error -0x%x\n", -err);
145+
#endif
146+
return mp_obj_new_str_from_vstr(&mp_type_bytes, &vstr);
147+
}
148+
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_ssl_errstr_obj, mod_ssl_errstr);
149+
150+
// _mbedtls_ssl_send is called by mbedtls to send bytes onto the underlying socket
119151
STATIC int _mbedtls_ssl_send(void *ctx, const byte *buf, size_t len) {
120152
mp_obj_t sock = *(mp_obj_t *)ctx;
121153

@@ -237,6 +269,8 @@ STATIC mp_obj_ssl_socket_t *socket_new(mp_obj_t sock, struct ssl_args *args) {
237269
}
238270
}
239271

272+
o->poll_flag = 0;
273+
o->poll_by_read = 0;
240274
if (args->do_handshake.u_bool) {
241275
while ((ret = mbedtls_ssl_handshake(&o->ssl)) != 0) {
242276
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
@@ -289,12 +323,16 @@ STATIC void socket_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kin
289323
STATIC mp_uint_t socket_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errcode) {
290324
mp_obj_ssl_socket_t *o = MP_OBJ_TO_PTR(o_in);
291325

326+
o->poll_flag &= ~READ_NEEDS_WRITE; // clear flag
292327
int ret = mbedtls_ssl_read(&o->ssl, buf, size);
293328
if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
294329
// end of stream
295330
return 0;
296331
}
297332
if (ret >= 0) {
333+
// if we got all we wanted, for the next poll try a read first 'cause
334+
// there may be data in the mbedtls record buffer
335+
o->poll_by_read = ret == size;
298336
return ret;
299337
}
300338
if (ret == MBEDTLS_ERR_SSL_WANT_READ) {
@@ -303,6 +341,7 @@ STATIC mp_uint_t socket_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errc
303341
// If handshake is not finished, read attempt may end up in protocol
304342
// wanting to write next handshake message. The same may happen with
305343
// renegotation.
344+
o->poll_flag |= READ_NEEDS_WRITE; // set flag
306345
ret = MP_EWOULDBLOCK;
307346
}
308347
*errcode = ret;
@@ -312,6 +351,7 @@ STATIC mp_uint_t socket_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errc
312351
STATIC mp_uint_t socket_write(mp_obj_t o_in, const void *buf, mp_uint_t size, int *errcode) {
313352
mp_obj_ssl_socket_t *o = MP_OBJ_TO_PTR(o_in);
314353

354+
o->poll_flag &= ~WRITE_NEEDS_READ; // clear flag
315355
int ret = mbedtls_ssl_write(&o->ssl, buf, size);
316356
if (ret >= 0) {
317357
return ret;
@@ -322,6 +362,7 @@ STATIC mp_uint_t socket_write(mp_obj_t o_in, const void *buf, mp_uint_t size, in
322362
// If handshake is not finished, write attempt may end up in protocol
323363
// wanting to read next handshake message. The same may happen with
324364
// renegotation.
365+
o->poll_flag |= WRITE_NEEDS_READ; // set flag
325366
ret = MP_EWOULDBLOCK;
326367
}
327368
*errcode = ret;
@@ -348,6 +389,41 @@ STATIC mp_uint_t socket_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg, i
348389
mbedtls_ssl_config_free(&self->conf);
349390
mbedtls_ctr_drbg_free(&self->ctr_drbg);
350391
mbedtls_entropy_free(&self->entropy);
392+
} else if (request == MP_STREAM_POLL) {
393+
mp_uint_t ret = 0;
394+
// If the last read returned everything asked for there may be more in the mbedtls buffer,
395+
// so find out. (There doesn't seem to be an equivalent issue with writes.)
396+
if ((arg & MP_STREAM_POLL_RD) && self->poll_by_read) {
397+
size_t avail = mbedtls_ssl_get_bytes_avail(&self->ssl);
398+
if (avail > 0) ret = MP_STREAM_POLL_RD;
399+
}
400+
// If we're polling to read but not write but mbedtls previously said it needs to write in
401+
// order to be able to read then poll for both and if either is available pretend the socket
402+
// is readable. When the app then performs a read, mbedtls is happy to perform the writes as
403+
// well. Essentially, what we're ensuring is that one of mbedtls' read/write functions is
404+
// called as soon as the socket can do something.
405+
if ((arg & MP_STREAM_POLL_RD) && !(arg & MP_STREAM_POLL_WR) &&
406+
self->poll_flag & READ_NEEDS_WRITE) {
407+
arg |= MP_STREAM_POLL_WR;
408+
ret |= mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
409+
if (ret & MP_STREAM_POLL_WR) {
410+
ret |= MP_STREAM_POLL_RD;
411+
ret &= ~MP_STREAM_POLL_WR;
412+
}
413+
return ret;
414+
// Now comes the same logic flipped around for write
415+
} else if ((arg & MP_STREAM_POLL_WR) && !(arg & MP_STREAM_POLL_RD) &&
416+
self->poll_flag & WRITE_NEEDS_READ) {
417+
arg |= MP_STREAM_POLL_RD;
418+
ret |= mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
419+
if (ret & MP_STREAM_POLL_RD) {
420+
ret |= MP_STREAM_POLL_WR;
421+
ret &= ~MP_STREAM_POLL_RD;
422+
}
423+
return ret;
424+
}
425+
// Pass down to underlying socket
426+
return ret | mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
351427
}
352428
// Pass all requests down to the underlying socket
353429
return mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
@@ -409,6 +485,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_ssl_wrap_socket_obj, 1, mod_ssl_wrap_socke
409485
STATIC const mp_rom_map_elem_t mp_module_ssl_globals_table[] = {
410486
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ussl) },
411487
{ MP_ROM_QSTR(MP_QSTR_wrap_socket), MP_ROM_PTR(&mod_ssl_wrap_socket_obj) },
488+
{ MP_ROM_QSTR(MP_QSTR_errstr), MP_ROM_PTR(&mod_ssl_errstr_obj) },
412489
};
413490

414491
STATIC MP_DEFINE_CONST_DICT(mp_module_ssl_globals, mp_module_ssl_globals_table);

extmod/uasyncio/stream.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
from . import core
55

6+
try:
7+
import ssl as modssl # module is used in function that has an ssl parameter
8+
except:
9+
modssl = None
610

711
class Stream:
812
def __init__(self, s, e={}):
@@ -71,20 +75,36 @@ async def drain(self):
7175

7276

7377
# Create a TCP stream connection to a remote host
74-
async def open_connection(host, port):
78+
async def open_connection(host, port, ssl=None, server_hostname=None):
7579
from uerrno import EINPROGRESS
7680
import usocket as socket
7781

7882
ai = socket.getaddrinfo(host, port)[0] # TODO this is blocking!
7983
s = socket.socket()
8084
s.setblocking(False)
81-
ss = Stream(s)
8285
try:
8386
s.connect(ai[-1])
8487
except OSError as er:
8588
if er.args[0] != EINPROGRESS:
8689
raise er
87-
yield core._io_queue.queue_write(s)
90+
# wrap with SSL, if requested
91+
if ssl:
92+
if not modssl:
93+
raise ValueError("SSL not supported")
94+
if ssl is True:
95+
ssl = {} # spec says to use ssl.create_default_context() but we don't have that
96+
elif isinstance(ssl, dict):
97+
# non-standard: accept dict with KW args suitable to call ssl.wrap_socket()
98+
if server_hostname:
99+
# spec: server_hostname sets or overrides the hostname that the target server’s
100+
# certificate will be matched against.
101+
ssl["server_hostname"] = server_hostname
102+
else:
103+
# spec says we should handle ssl.SSLContext object, but ain't got that
104+
raise ValueError("invalid ssl param")
105+
ssl["do_handshake"] = False # as non-blocking as possible
106+
s = modssl.wrap_socket(s, **ssl)
107+
ss = Stream(s)
88108
return ss, ss
89109

90110

ports/esp32/modsocket.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,10 @@ STATIC mp_obj_t socket_connect(const mp_obj_t arg0, const mp_obj_t arg1) {
362362
MP_THREAD_GIL_ENTER();
363363
lwip_freeaddrinfo(res);
364364
if (r != 0) {
365-
mp_raise_OSError(errno);
365+
// side-note: LwIP internally doesn't seem to have an error code for ECONNREFUSED and
366+
// so refused connections show up as ECONNRESET. Could be band-aided for blocking connect,
367+
// harder to do for nonblocking.
368+
exception_from_errno(errno);
366369
}
367370

368371
return mp_const_none;

tests/multi_net/ssl_data.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,58 @@
11
# Simple test creating an SSL connection and transferring some data
22
# This test won't run under CPython because it requires key/cert
33

4-
import usocket as socket, ussl as ssl
4+
try:
5+
import usocket as socket, ussl as ssl, ubinascii as binascii, uselect as select
6+
except ModuleNotFoundError:
7+
import socket, ssl, binascii, select
58

69
PORT = 8000
710

11+
# This self-signed key/cert pair is randomly generated and to be used for
12+
# testing/demonstration only.
13+
# openssl req -x509 -newkey rsa:1024 -keyout key.pem -out cert.pem -days 36500 -nodes
14+
cert = """
15+
-----BEGIN CERTIFICATE-----
16+
MIICaDCCAdGgAwIBAgIUaYEwlY581HuPWHm2ndTWejuggAIwDQYJKoZIhvcNAQEL
17+
BQAwRTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
18+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yMDA0MTgxOTAwMDBaGA8yMTIw
19+
MDMyNTE5MDAwMFowRTELMAkGA1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUx
20+
ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCBnzANBgkqhkiG9w0B
21+
AQEFAAOBjQAwgYkCgYEAxmACtMgGR2tTKVHzxG67Yx61pWNynXUE0q00yJ0a34AK
22+
uQKzvyEdvkk5lL3snV4N5wKeRgWmS3/krl/YQO+Rk4eSJRqJc8INd3qSOFSNUgPg
23+
W0VPP9vPox8au5Ngqn06jgtdD1F0a6Z+f+N3+JyRPAaetIWlFC9WEn+zzz0/cmkC
24+
AwEAAaNTMFEwHQYDVR0OBBYEFBaI7GVj4GjxPWq+RO7A/4INOq2RMB8GA1UdIwQY
25+
MBaAFBaI7GVj4GjxPWq+RO7A/4INOq2RMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI
26+
hvcNAQELBQADgYEAMpdYd8jkWxoXMxV+X2rpyx/BnPrPa+l2LehlulrU7lRh4QIU
27+
t4f+W+yBvkFscPatpRfJoXXqregmhLxo8poKw08pjn7DNKBzcsPsxnmRIvFZuL2J
28+
wYHGyP9HcMpsnx+UW2YjjQ4R1I0smRI7ZKiax8AJkN/P9eHH9Xku6ostXYk=
29+
-----END CERTIFICATE-----
30+
"""
31+
key = """
32+
-----BEGIN PRIVATE KEY-----
33+
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBAMZgArTIBkdrUylR
34+
88Ruu2MetaVjcp11BNKtNMidGt+ACrkCs78hHb5JOZS97J1eDecCnkYFpkt/5K5f
35+
2EDvkZOHkiUaiXPCDXd6kjhUjVID4FtFTz/bz6MfGruTYKp9Oo4LXQ9RdGumfn/j
36+
d/ickTwGnrSFpRQvVhJ/s889P3JpAgMBAAECgYBPkxnizM3//iRY0d/37vdKFnqF
37+
AnRqhxNNM1+WDbdG6kTi3BugUrdsqlDnwpvUsHLhNOKqcf+4D3B7JkVIHxGEqLSl
38+
YMbQrldodPwIP0ycf9hegzuhEvuYGkex22edmQ5brkdIt6QCv0QRtProYowJx4p6
39+
CuM5423ORejs6Vw9gQJBAOF//1Ovmm5Q1d90ZzjFhZCwG3/z5uwqZMGBxJTaibSC
40+
O5cci3n9Tcc4AebnMf5eyrXHovtSg1FfDxS+IUccXRECQQDhNM3R31YvYmRZwrTn
41+
f71y+buXpUtMDUDhFK8FNZN1/zJ6dJVrWQ/MVj+TaNjLUYNdPmRPHQdt8+Fx65y9
42+
95/ZAkEAqgmkdGwz3P9jZm4V778xqhrBgche1rJY63l4zG3F7LFPUfEaU1BoN9LJ
43+
zF2FWzQLUutIwI5FqzQs4Q1FdqOyoQJBALAL1iUMwFO0R5v/X+lj6xXY8PM/jJf7
44+
+E67G4In+okQIEanojJTYc0rUvGJ0YdGxjj6z/EkUS17qy2hsFq0GykCQQCiucp9
45+
7kbPpzw/gW+ERfoLgtZKrP/+Au9C5sz2wxUpeKhYihVePF8pmytyD8mqt/3LIJhZ
46+
NA2FEss2+KJUCjHc
47+
-----END PRIVATE KEY-----
48+
"""
49+
chain = cert + key
50+
# Produce cert/key for MicroPython
51+
cert = cert[cert.index("M"):cert.index("=")+2]
52+
key = key[key.index("M"):key.rstrip().rindex("\n")+1]
53+
cert = binascii.a2b_base64(cert)
54+
key = binascii.a2b_base64(key)
55+
856

957
# Server
1058
def instance0():
@@ -15,7 +63,15 @@ def instance0():
1563
s.listen(1)
1664
multitest.next()
1765
s2, _ = s.accept()
18-
s2 = ssl.wrap_socket(s2, server_side=True)
66+
if hasattr(ssl, 'SSLContext'):
67+
fn = '/tmp/MP_test_cert.pem'
68+
with open(fn, "w") as f:
69+
f.write(chain)
70+
ctx = ssl.SSLContext()
71+
ctx.load_cert_chain(fn)
72+
s2 = ctx.wrap_socket(s2, server_side=True)
73+
else:
74+
s2 = ssl.wrap_socket(s2, server_side=True, key=key, cert=cert)
1975
print(s2.read(16))
2076
s2.write(b"server to client")
2177
s.close()

tests/multi_net/ssl_data.py.exp

Lines changed: 0 additions & 4 deletions
This file was deleted.

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy