Utilizar SSL

asyncio tiene soporte incorporado para habilitar la comunicación SSL en conectores, Pasar una instancia SSLContext a las co-rutinas que crean conexiones de servidor o cliente habilita el soporte y asegura que la configuración del protocolo SSL se realiza antes de que el conector se presenta como listo para el uso de la aplicación.

El servidor de eco basado en co-rutina y el cliente de la sección anterior se pueden actualizar con unos pequeños cambios. El primer paso es crear el certificado y los archivos de llaves. Un certificado auto-firmado puede ser creado con un comando como:

$ openssl req -newkey rsa:2048 -nodes -keyout pymotw.key \
-x509 -days 365 -out pymotw.crt

El comando openssl pedirá varios valores que se utilizan para generar el certificado, y luego produce los archivos de salida requeridos.

La configuración insegura de conector en el servidor ejemplo anterior usa start_server() para crear el conector de escucha.

factory = asyncio.start_server(echo, *SERVER_ADDRESS)
server = event_loop.run_until_complete(factory)

Para agregar encriptación, crea un SSLContext con el certificado y la llave acabas de generar y luego pasa el contexto a start_server().

# The certificate is created with pymotw.com as the hostname,
# which will not match when the example code runs elsewhere,
# so disable hostname verification.
ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_context.check_hostname = False
ssl_context.load_cert_chain('pymotw.crt', 'pymotw.key')

# Create the server and let the loop finish the coroutine before
# starting the real event loop.
factory = asyncio.start_server(echo, *SERVER_ADDRESS,
                               ssl=ssl_context)

Se necesitan cambios similares en el cliente. La versión antigua usa open_connection() para crear el conector conectado al servidor.

    reader, writer = await asyncio.open_connection(*address)

Se necesita un SSLContext nuevamente para asegurar el lado del cliente del conector. La identidad del cliente no se está haciendo cumplir, por lo que solo se necesita cargar el certificado.

    # The certificate is created with pymotw.com as the hostname,
    # which will not match when the example code runs
    # elsewhere, so disable hostname verification.
    ssl_context = ssl.create_default_context(
        ssl.Purpose.SERVER_AUTH,
    )
    ssl_context.check_hostname = False
    ssl_context.load_verify_locations('pymotw.crt')
    reader, writer = await asyncio.open_connection(
        *server_address, ssl=ssl_context)

Se necesitan otros pequeños cambios en el cliente. Porque la conexión SSL no admite el envío de un final de archivo (EOF), el cliente utiliza un byte NULO como un terminador de mensaje en su lugar.

La versión anterior del bucle de envío del cliente usa write_eof().

    # This could be writer.writelines() except that
    # would make it harder to show each part of the message
    # being sent.
    for msg in messages:
        writer.write(msg)
        log.debug('sending {!r}'.format(msg))
    if writer.can_write_eof():
        writer.write_eof()
    await writer.drain()

La nueva versión envía un byte cero (b'\x00').

    # This could be writer.writelines() except that
    # would make it harder to show each part of the message
    # being sent.
    for msg in messages:
        writer.write(msg)
        log.debug('sending {!r}'.format(msg))
    # SSL does not support EOF, so send a null byte to indicate
    # the end of the message.
    writer.write(b'\x00')
    await writer.drain()

La co-rutina echo() en el servidor debe buscar el byte NULL y cerrar la conexión del cliente cuando se reciba.

async def echo(reader, writer):
    address = writer.get_extra_info('peername')
    log = logging.getLogger('echo_{}_{}'.format(*address))
    log.debug('connection accepted')
    while True:
        data = await reader.read(128)
        terminate = data.endswith(b'\x00')
        data = data.rstrip(b'\x00')
        if data:
            log.debug('received {!r}'.format(data))
            writer.write(data)
            await writer.drain()
            log.debug('sent {!r}'.format(data))
        if not data or terminate:
            log.debug('message terminated, closing connection')
            writer.close()
            return

Ejecutar el servidor en una ventana, y el cliente en otra, produce esta salida.

$ python3 asyncio_echo_server_ssl.py
asyncio: Using selector: KqueueSelector
main: starting up on localhost port 10000
echo_::1_53957: connection accepted
echo_::1_53957: received b'This is the message. '
echo_::1_53957: sent b'This is the message. '
echo_::1_53957: received b'It will be sent in parts.'
echo_::1_53957: sent b'It will be sent in parts.'
echo_::1_53957: message terminated, closing connection
$ python3 asyncio_echo_client_ssl.py
asyncio: Using selector: KqueueSelector
echo_client: connecting to localhost port 10000
echo_client: sending b'This is the message. '
echo_client: sending b'It will be sent '
echo_client: sending b'in parts.'
echo_client: waiting for response
echo_client: received b'This is the message. '
echo_client: received b'It will be sent in parts.'
echo_client: closing
main: closing event loop