8000 Merge moduasyncio: Add SSL support #5840 · tve/micropython@c7e6bfe · GitHub
[go: up one dir, main page]

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 10000 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+ACrkCs78hHb5JOZS97J1eDecCnkY 10000 Fpkt/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)
0