Synss / python-mbedtls

Cryptographic library with an mbed TLS back end
MIT License
79 stars 28 forks source link

Need threaded DTLS server example #20

Closed inpos closed 5 years ago

inpos commented 5 years ago

Hi. Very need threaded server example for DTLS. Thanks.

Synss commented 5 years ago

Hi Roman,

Putting the example from the readme in an infinite loop should do. Let me see if I can find a simple example.

Regards, Mathias

inpos commented 5 years ago

Hi Mathias. No, after first accept and start communication in other thread, second accept block forever and don't accept any new connections.

By the way, tls.(D)TLSConfiguration live only and only in local namespace. Even in instance of some class. After, for ex., self.conf = tls.DTLSConfiguration() in one method accessing the self.conf in other method of this instance raise "Buffer out of memory". And accessing wrapped socket in other method raise the same exception. Work only if init variable with tls.(D)TLSConfiguration as global variable and wrap with it socket in instance.

Synss commented 5 years ago

Hi, thank you for your feedback. I will have to look into the D/TLSConfiguration bug.

Here are not-so-short but tested examples of a DTLS server and a DTLS client that work on my machine. The code is probably not that great but this is what I have used to implement DTLS.

Listening on "0.0.0.0" for the server is important. It will not work if you only listen on "127.0.0.1" for example. This is because DTLS accept() steals the first client socket to handshake and communicate with the client. The server then bind()s another socket for the next client.

This is also what happens in net_socket.c from upstream libmbedtls and I do not know of a better way to handle handshake over UDP...

#!/usr/bin/env python

"""Example DTLS server"""

import datetime as dt
import socket
import struct

import mbedtls.hash as hashlib
from mbedtls.pk import RSA, ECC
from mbedtls.x509 import BasicConstraints, CRT, CSR
from mbedtls.tls import *
from mbedtls.tls import _enable_debug_output, _set_debug_level

now = dt.datetime.utcnow()
digestmod = hashlib.sha256

ca0_key = RSA()
ca0_key.generate()
ca1_key = ECC()
ca1_key.generate()
ee0_key = ECC()
ee0_key.generate()

ca0_crt = CRT.selfsign(
    CSR.new(ca0_key, "CN=Trusted CA", digestmod()),
    ca0_key,
    not_before=now,
    not_after=now + dt.timedelta(days=90),
    serial_number=0x123456,
    basic_constraints=BasicConstraints(True, -1),
)
ca1_crt = ca0_crt.sign(
    CSR.new(ca1_key, "CN=Intermediate CA", digestmod()),
    ca0_key,
    not_before=now,
    not_after=now + dt.timedelta(days=90),
    serial_number=0x234567,
    basic_constraints=BasicConstraints(True, -1),
)
ee0_crt = ca1_crt.sign(
    CSR.new(ee0_key, "CN=End Entity", digestmod()),
    ca1_key,
    not_before=now,
    not_after=now + dt.timedelta(days=90),
    serial_number=0x345678,
)

with open("ca0.crt", "wt") as ca:
    ca.write(ca0_crt.to_PEM())

trust_store = TrustStore()
trust_store.add(ca0_crt)

def block(cb, *args, **kwargs):
    while True:
        try:
            result = cb(*args, **kwargs)
        except (WantReadError, WantWriteError):
            print(" .", cb.__name__)
        else:
            print(" .", "done", cb.__name__, result)
            return result

conf = DTLSConfiguration(
    trust_store=trust_store,
    certificate_chain=([ee0_crt, ca1_crt], ee0_key),
    validate_certificates=False,
)

_enable_debug_output(conf)
_set_debug_level(1)

def echo_until(sock, end):
    cli0, cli_address0 = sock.accept()
    cli0.setcookieparam(cli_address0[0].encode("ascii"))
    try:
        block(cli0.do_handshake)
    except HelloVerifyRequest:
        print("HVR")

    cli1, cli_address1 = cli0.accept()
    cli0.close()
    cli1.setcookieparam(cli_address1[0].encode("ascii"))
    block(cli1.do_handshake)
    print(" .", "handshake", cli1.negotiated_tls_version())

    cli = cli1

    while True:
        data = block(cli.recv, 4096)
        print(" .", "R", data)
        nn = block(cli.send, data)
        print(" .", "S", nn, len(data))
        if data == end:
            break

    print(" .", "done")
    print(cli)
    cli.close()

address = ("0.0.0.0", 4433)
host, port = address

ctx = ServerContext(conf)
srv = ctx.wrap_socket(
    socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
)

print(" .", "bind", srv, address)
srv.bind(address)

while True:
    print(" .", ">>>")
    echo_until(srv, b"\0")
    print(" .", "<<<")
#!/usr/bin/env python

"""Example DTLS client"""

import socket
import struct

from mbedtls.x509 import CRT
from mbedtls.tls import *
from mbedtls.tls import _enable_debug_output, _set_debug_level

with open("ca0.crt", "rt") as ca:
    ca0_crt = CRT.from_PEM(ca.read())

trust_store = TrustStore()
trust_store.add(ca0_crt)

conf = DTLSConfiguration(trust_store=trust_store, validate_certificates=False)

_enable_debug_output(conf)
_set_debug_level(1)

address = ("127.0.0.1", 4433)
host, port = address

ctx = ClientContext(conf)
cli = ctx.wrap_socket(
    socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP),
    server_hostname="localhost",
)

print(" .", "connect", address)
cli.connect(address)

def block(cb, *args, **kwargs):
    while True:
        try:
            result = cb(*args, **kwargs)
        except (WantReadError, WantWriteError):
            print(" .", cb.__name__)
        else:
            print(" .", "done", cb.__name__, result)
            return result

block(cli.do_handshake)
print(" .", "handshake", cli.negotiated_tls_version())

msg = b"hello"
for _ in range(1):
    nn = block(cli.send, msg)
    print(" .", "S", nn, len(msg))
    data, addr = block(cli.recvfrom, 4096)
    print(" .", "R", nn, data)
else:
    block(cli.send, b"\0")
    block(cli.recvfrom, 4096)

print(cli)
cli.close()
Synss commented 5 years ago

By the way, tls.(D)TLSConfiguration live only and only in local namespace. Even in instance of some class. After, for ex., self.conf = tls.DTLSConfiguration() in one method accessing the self.conf in other method of this instance raise "Buffer out of memory". And accessing wrapped socket in other method raise the same exception.

I cannot reproduce this behaviour. Could you give me a short example reproducing the bug?

inpos commented 5 years ago

I cannot reproduce this behaviour. Could you give me a short example reproducing the bug?

Here:

import socket
from mbedtls import tls
import datetime as dt
from mbedtls import hash as hashlib
from mbedtls import pk
from mbedtls import x509
from uuid import uuid4

class DTLSCerts:
    def __init__(self):
        now = dt.datetime.utcnow()
        self.ca0_key = pk.RSA()
        _ = self.ca0_key.generate()
        ca0_csr = x509.CSR.new(self.ca0_key, 'CN=thrusted CA', hashlib.sha256())
        self.ca0_crt = x509.CRT.selfsign(
            ca0_csr, self.ca0_key,
            not_before=now, not_after=now + dt.timedelta(days=3650),
            serial_number=0x123456,
            basic_constraints=x509.BasicConstraints(True, 1))
        self.ca1_key = pk.ECC()
        _ = self.ca1_key.generate()
        ca1_csr = x509.CSR.new(self.ca1_key, 'CN=intermediate CA', hashlib.sha256())
        self.ca1_crt = self.ca0_crt.sign(
            ca1_csr, self.ca0_key, now, now + dt.timedelta(days=3650), 0x123456,
            basic_constraints=x509.BasicConstraints(ca=True, max_path_length=3))
        self.srv_crt, self.srv_key = self.server_cert()
    def server_cert(self):
        now = dt.datetime.utcnow()
        ee0_key = pk.ECC()
        _ = ee0_key.generate()
        ee0_csr = x509.CSR.new(ee0_key, f'CN=peer [{uuid4().hex}]', hashlib.sha256())
        ee0_crt = self.ca1_crt.sign(
            ee0_csr, self.ca1_key, now, now + dt.timedelta(days=3650), 0x987654)
        return ee0_crt, ee0_key

dtls_certs = DTLSCerts()
trust_store = tls.TrustStore()
trust_store.add(dtls_certs.ca0_crt)

class DTLSServer:
    def __init__(self, listener_timeout = 0.5, reuse_addr = True):
        srv_crt, srv_key = dtls_certs.server_cert()
        dtls_srv_ctx = tls.ServerContext(tls.DTLSConfiguration(
            trust_store=trust_store,
            certificate_chain=([srv_crt, dtls_certs.ca1_crt], srv_key),
            validate_certificates=False,
            ))
        dtls_srv = dtls_srv_ctx.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_DGRAM))
        dtls_srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        dtls_srv.bind(('', 6060))
        self.listener = dtls_srv
        print(self.listener.context.configuration)
        print(self.listener.context.configuration.certificate_chain)
    def test(self):
        print(self.listener.context.configuration)
        print(self.listener.context.configuration.certificate_chain)

>>> s = DTLSServer()
DTLSConfiguration(validate_certificates=False, certificate_chain=((<mbedtls.x509.CRT object at 0x560ea30fc768>, <mbedtls.x509.CRT object at 0x560ea31057f8>), <mbedtls.pk.ECC object at 0x7f136a9b0508>), ciphers=[], inner_protocols=(), lowest_supported_version=<DTLSVersion.DTLSv1_0: 2>, highest_supported_version=<DTLSVersion.DTLSv1_2: 3>, trust_store=TrustStore([<mbedtls.x509.CRT object at 0x560ea3105a68>]), sni_callback=None)
((<mbedtls.x509.CRT object at 0x560ea30fc768>, <mbedtls.x509.CRT object at 0x560ea31057f8>), <mbedtls.pk.ECC object at 0x7f136a9b0508>)
>>> s.test()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 16, in test
  File "src/mbedtls/tls.pyx", line 352, in mbedtls.tls._BaseConfiguration.__repr__
  File "src/mbedtls/tls.pyx", line 406, in mbedtls.tls._BaseConfiguration.certificate_chain.__get__
  File "src/mbedtls/x509.pyx", line 409, in mbedtls.x509.CRT.from_DER
IndexError: Out of bounds on buffer access (axis 0)
>>> print(s.listener.context)
<mbedtls.tls.ServerContext object at 0x560ea310d2f8>
>>> print(s.listener.context.configuration.certificate_chain)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "src/mbedtls/tls.pyx", line 406, in mbedtls.tls._BaseConfiguration.certificate_chain.__get__
  File "src/mbedtls/x509.pyx", line 409, in mbedtls.x509.CRT.from_DER
IndexError: Out of bounds on buffer access (axis 0)
>>> print(s.listener.context.configuration.)
s.listener.context.configuration.anti_replay                s.listener.context.configuration.lowest_supported_version
s.listener.context.configuration.certificate_chain          s.listener.context.configuration.sni_callback
s.listener.context.configuration.ciphers                    s.listener.context.configuration.trust_store
s.listener.context.configuration.highest_supported_version  s.listener.context.configuration.update(
s.listener.context.configuration.inner_protocols            s.listener.context.configuration.validate_certificates
>>> print(s.listener.context.configuration.highest_supported_version)
DTLSVersion.DTLSv1_2
inpos commented 5 years ago

Therefore, on handshake server can't find own certificate and raise exception with message that there is no suitable ciphers.

Synss commented 5 years ago

Indeed, I will look into it. Thank you.

stepheny commented 5 years ago
class DTLSServer:
    def __init__(self, listener_timeout = 0.5, reuse_addr = True):
        srv_crt, srv_key = dtls_certs.server_cert()
...

The very last problem probably came from here. You've defined srv_crt, srv_key as local variable instead of DTLSServer member. Whence DTLSServer.__init__ finished, the instance would lost refs to them, thus they would be freed. Replace srv_crt, srv_key with self.srv_crt, self.srv_key, and your code should work as expected.

stepheny commented 5 years ago
ctx = ServerContext(conf)
srv = ctx.wrap_socket(
    socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
)

print(" .", "bind", srv, address)
srv.bind(address)

while True:
    print(" .", ">>>")
    echo_until(srv, b"\0")
    print(" .", "<<<")

srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) should be called before srv.bind(address)

Synss commented 5 years ago

@stepheny

Replace srv_crt, srv_key with self.srv_crt, self.srv_key, and your code should work as expected.

This is correct, there is an ownership problem and I am not always reporting errors in an understandable way. I will be working on it for the next release.

Thank you for your help!

inpos commented 5 years ago

Last question. DTLS don't allow partial recv? I mean, if one peer sent 4096 bytes packet and second recv 100 bytes chunk then next recv raise SSL lower protocol error. Is this correct?

Synss commented 5 years ago

This is not related with encryption but with the underlying protocols. UDP works with datagrams. You should pretty much always try to receive a full datagram or consider it lost. You can always try to pass the MSG_PEEK flag to the socket for a partial read... or use TCP.

inpos commented 5 years ago

I thought that in DTLS integrity is monitored and something similar to the stream is provided. I implemented these functions in my project. Thanks for answers.

Synss commented 5 years ago

These bugs should be fixed on the master branch now. I'll release sometime this week or on the week end now.

Thank you for reporting bugs!

Do not hesitate to give some more feedback if you can.

inpos commented 5 years ago

In this part of code:

def echo_until(sock, end):
    cli0, cli_address0 = sock.accept()
    cli0.setcookieparam(cli_address0[0].encode("ascii"))
    try:
        block(cli0.do_handshake)
    except HelloVerifyRequest:
        print("HVR")

    cli1, cli_address1 = cli0.accept()
    cli0.close()
    cli1.setcookieparam(cli_address1[0].encode("ascii"))
    block(cli1.do_handshake)
    print(" .", "handshake", cli1.negotiated_tls_version())

we are suddenly seeing

cli0.close()

This MUST be documented. Without this close second accept() blocks forever.

Synss commented 5 years ago

we are suddenly seeing

cli0.close() This MUST be documented.

You can reuse the name

def echo_until(sock, end):
    cli, cli_address = sock.accept()
    cli.setcookieparam(cli_address[0].encode("ascii"))
    try:
        block(cli.do_handshake)
    except HelloVerifyRequest:
        print("HVR")

    cli, cli_address = cli.accept()
    cli.setcookieparam(cli_address[0].encode("ascii"))
    block(cli.do_handshake)
    print(" .", "handshake", cli.negotiated_tls_version())

and let the ref count take care of the first connection. This is what is done in the README (dtls_server_main_loop()) and it works as well.

I could try to clean up my example programs and distribute them as well, like libmbedtls does. My problem with that is that extra example programs are not easy to test and I am afraid they go out of sync without notice.

DTLS is tough because UDP offers so little guarantees. Datagrams may even be lost or reordered during the handshake. This makes the handshake difficult or easily abused, see RFC4347.

libmbedtls makes the cookie optional (i.e. the first connection (cli0) would be usable) but leaving the cookie out makes any such DTLS server a potential vector in DOS attacks against third party servers. With the cookie, you first get a connection that only informs the server that some client wants to connect and a second one that is finally usable. This is just the way a DTLS handshake works server side. The good thing is that it is transparent to the clients.

There really is not much I can do about it I am afraid.