Skip to content

Commit 7d73e49

Browse files
committed
moduasyncio: Add SSL support.
1 parent cdaec0d commit 7d73e49

File tree

8 files changed

+343
-48
lines changed

8 files changed

+343
-48
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 & 36 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 {
@@ -76,46 +87,29 @@ STATIC void mbedtls_debug(void *ctx, int level, const char *file, int line, cons
7687
}
7788
#endif
7889

79-
STATIC NORETURN void mbedtls_raise_error(int err) {
80-
// _mbedtls_ssl_send and _mbedtls_ssl_recv (below) turn positive error codes from the
81-
// underlying socket into negative codes to pass them through mbedtls. Here we turn them
82-
// positive again so they get interpreted as the OSError they really are. The
83-
// cut-off of -256 is a bit hacky, sigh.
84-
if (err < 0 && err > -256) {
85-
mp_raise_OSError(-err);
86-
}
87-
88-
#if defined(MBEDTLS_ERROR_C)
89-
// Including mbedtls_strerror takes about 1.5KB due to the error strings.
90-
// MBEDTLS_ERROR_C is the define used by mbedtls to conditionally include mbedtls_strerror.
91-
// It is set/unset in the MBEDTLS_CONFIG_FILE which is defined in the Makefile.
92-
93-
// Try to allocate memory for the message
94-
#define ERR_STR_MAX 80 // mbedtls_strerror truncates if it doesn't fit
95-
mp_obj_str_t *o_str = m_new_obj_maybe(mp_obj_str_t);
96-
byte *o_str_buf = m_new_maybe(byte, ERR_STR_MAX);
97-
if (o_str == NULL || o_str_buf == NULL) {
98-
mp_raise_OSError(err);
90+
// mod_ssl_errstr returns the error string corresponding to the error code found in an OSError,
91+
// such as returned by read/write.
92+
STATIC mp_obj_t mod_ssl_errstr(mp_obj_t err_in) {
93+
size_t err = mp_obj_get_int(err_in);
94+
vstr_t vstr;
95+
vstr_init_len(&vstr, 80);
96+
97+
// Including mbedtls_strerror takes about 16KB on the esp32 due to all the strings
98+
#if 1
99+
vstr.buf[0] = 0;
100+
mbedtls_strerror(err, vstr.buf, vstr.alloc);
101+
vstr.len = strlen(vstr.buf);
102+
if (vstr.len == 0) {
103+
return MP_OBJ_NULL;
99104
}
100-
101-
// print the error message into the allocated buffer
102-
mbedtls_strerror(err, (char *)o_str_buf, ERR_STR_MAX);
103-
size_t len = strlen((char *)o_str_buf);
104-
105-
// Put the exception object together
106-
o_str->base.type = &mp_type_str;
107-
o_str->data = o_str_buf;
108-
o_str->len = len;
109-
o_str->hash = qstr_compute_hash(o_str->data, o_str->len);
110-
// raise
111-
mp_obj_t args[2] = { MP_OBJ_NEW_SMALL_INT(err), MP_OBJ_FROM_PTR(o_str)};
112-
nlr_raise(mp_obj_exception_make_new(&mp_type_OSError, 2, 0, args));
113105
#else
114-
// mbedtls is compiled without error strings so we simply return the err number
115-
mp_raise_OSError(err); // err is typically a large negative number
106+
vstr_printf(vstr, "mbedtls error -0x%x\n", -err);
116107
#endif
108+
return mp_obj_new_str_from_vstr(&mp_type_bytes, &vstr);
117109
}
110+
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_ssl_errstr_obj, mod_ssl_errstr);
118111

112+
// _mbedtls_ssl_send is called by mbedtls to send bytes onto the underlying socket
119113
STATIC int _mbedtls_ssl_send(void *ctx, const byte *buf, size_t len) {
120114
mp_obj_t sock = *(mp_obj_t *)ctx;
121115

@@ -237,6 +231,8 @@ STATIC mp_obj_ssl_socket_t *socket_new(mp_obj_t sock, struct ssl_args *args) {
237231
}
238232
}
239233

234+
o->poll_flag = 0;
235+
o->poll_by_read = 0;
240236
if (args->do_handshake.u_bool) {
241237
while ((ret = mbedtls_ssl_handshake(&o->ssl)) != 0) {
242238
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE) {
@@ -263,7 +259,7 @@ STATIC mp_obj_ssl_socket_t *socket_new(mp_obj_t sock, struct ssl_args *args) {
263259
} else if (ret == MBEDTLS_ERR_X509_BAD_INPUT_DATA) {
264260
mp_raise_ValueError(MP_ERROR_TEXT("invalid cert"));
265261
} else {
266-
mbedtls_raise_error(ret);
262+
mp_raise_OSError(-ret);
267263
}
268264
}
269265

@@ -289,12 +285,16 @@ STATIC void socket_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kin
289285
STATIC mp_uint_t socket_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errcode) {
290286
mp_obj_ssl_socket_t *o = MP_OBJ_TO_PTR(o_in);
291287

288+
o->poll_flag &= ~READ_NEEDS_WRITE; // clear flag
292289
int ret = mbedtls_ssl_read(&o->ssl, buf, size);
293290
if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) {
294291
// end of stream
295292
return 0;
296293
}
297294
if (ret >= 0) {
295+
// if we got all we wanted, for the next poll try a read first 'cause
296+
// there may be data in the mbedtls record buffer
297+
o->poll_by_read = ret == size;
298298
return ret;
299299
}
300300
if (ret == MBEDTLS_ERR_SSL_WANT_READ) {
@@ -303,6 +303,7 @@ STATIC mp_uint_t socket_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errc
303303
// If handshake is not finished, read attempt may end up in protocol
304304
// wanting to write next handshake message. The same may happen with
305305
// renegotation.
306+
o->poll_flag |= READ_NEEDS_WRITE; // set flag
306307
ret = MP_EWOULDBLOCK;
307308
}
308309
*errcode = ret;
@@ -312,6 +313,7 @@ STATIC mp_uint_t socket_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errc
312313
STATIC mp_uint_t socket_write(mp_obj_t o_in, const void *buf, mp_uint_t size, int *errcode) {
313314
mp_obj_ssl_socket_t *o = MP_OBJ_TO_PTR(o_in);
314315

316+
o->poll_flag &= ~WRITE_NEEDS_READ; // clear flag
315317
int ret = mbedtls_ssl_write(&o->ssl, buf, size);
316318
if (ret >= 0) {
317319
return ret;
@@ -322,6 +324,7 @@ STATIC mp_uint_t socket_write(mp_obj_t o_in, const void *buf, mp_uint_t size, in
322324
// If handshake is not finished, write attempt may end up in protocol
323325
// wanting to read next handshake message. The same may happen with
324326
// renegotation.
327+
o->poll_flag |= WRITE_NEEDS_READ; // set flag
325328
ret = MP_EWOULDBLOCK;
326329
}
327330
*errcode = ret;
@@ -348,6 +351,43 @@ STATIC mp_uint_t socket_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg, i
348351
mbedtls_ssl_config_free(&self->conf);
349352
mbedtls_ctr_drbg_free(&self->ctr_drbg);
350353
mbedtls_entropy_free(&self->entropy);
354+
} else if (request == MP_STREAM_POLL) {
355+
mp_uint_t ret = 0;
356+
// If the last read returned everything asked for there may be more in the mbedtls buffer,
357+
// so find out. (There doesn't seem to be an equivalent issue with writes.)
358+
if ((arg & MP_STREAM_POLL_RD) && self->poll_by_read) {
359+
size_t avail = mbedtls_ssl_get_bytes_avail(&self->ssl);
360+
if (avail > 0) {
361+
ret = MP_STREAM_POLL_RD;
362+
}
363+
}
364+
// If we're polling to read but not write but mbedtls previously said it needs to write in
365+
// order to be able to read then poll for both and if either is available pretend the socket
366+
// is readable. When the app then performs a read, mbedtls is happy to perform the writes as
367+
// well. Essentially, what we're ensuring is that one of mbedtls' read/write functions is
368+
// called as soon as the socket can do something.
369+
if ((arg & MP_STREAM_POLL_RD) && !(arg & MP_STREAM_POLL_WR) &&
370+
self->poll_flag & READ_NEEDS_WRITE) {
371+
arg |= MP_STREAM_POLL_WR;
372+
ret |= mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
373+
if (ret & MP_STREAM_POLL_WR) {
374+
ret |= MP_STREAM_POLL_RD;
375+
ret &= ~MP_STREAM_POLL_WR;
376+
}
377+
return ret;
378+
// Now comes the same logic flipped around for write
379+
} else if ((arg & MP_STREAM_POLL_WR) && !(arg & MP_STREAM_POLL_RD) &&
380+
self->poll_flag & WRITE_NEEDS_READ) {
381+
arg |= MP_STREAM_POLL_RD;
382+
ret |= mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
383+
if (ret & MP_STREAM_POLL_RD) {
384+
ret |= MP_STREAM_POLL_WR;
385+
ret &= ~MP_STREAM_POLL_RD;
386+
}
387+
return ret;
388+
}
389+
// Pass down to underlying socket
390+
return ret | mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
351391
}
352392
// Pass all requests down to the underlying socket
353393
return mp_get_stream(self->sock)->ioctl(self->sock, request, arg, errcode);
@@ -409,6 +449,7 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_KW(mod_ssl_wrap_socket_obj, 1, mod_ssl_wrap_socke
409449
STATIC const mp_rom_map_elem_t mp_module_ssl_globals_table[] = {
410450
{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_ussl) },
411451
{ MP_ROM_QSTR(MP_QSTR_wrap_socket), MP_ROM_PTR(&mod_ssl_wrap_socket_obj) },
452+
{ MP_ROM_QSTR(MP_QSTR_errstr), MP_ROM_PTR(&mod_ssl_errstr_obj) },
412453
};
413454

414455
STATIC MP_DEFINE_CONST_DICT(mp_module_ssl_globals, mp_module_ssl_globals_table);

extmod/uasyncio/stream.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
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
10+
611

712
class Stream:
813
def __init__(self, s, e={}):
@@ -71,20 +76,36 @@ async def drain(self):
7176

7277

7378
# Create a TCP stream connection to a remote host
74-
async def open_connection(host, port):
79+
async def open_connection(host, port, ssl=None, server_hostname=None):
7580
from uerrno import EINPROGRESS
7681
import usocket as socket
7782

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

90111

ports/esp32/modsocket.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@ STATIC mp_obj_t socket_connect(const mp_obj_t arg0, const mp_obj_t arg1) {
355355
MP_THREAD_GIL_ENTER();
356356
lwip_freeaddrinfo(res);
357357
if (r != 0) {
358+
// side-note: LwIP internally doesn't seem to have an error code for ECONNREFUSED and
359+
// so refused connections show up as ECONNRESET. Could be band-aided for blocking connect,
360+
// harder to do for nonblocking.
358361
mp_raise_OSError(errno);
359362
}
360363

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