Mi IoT ha muerto: Cómo actualizar una instalación LUA sobre ESP8266 para que sea compatible con TLS 1.2

En Medir y registrar temperatura, humedad relativa y presión atmosférica con un ESP8266: el componente LUA realizo un montaje doméstico para monitorizar la temperatura y humedad de mi casa. Ese montaje ha estado funcionando, sin apenas variaciones y sin incidencias, desde 2016. Casi ocho años. No está mal.

El problema es que acabo de actualizar las zonas SmartOS de mis servidores a la versión 2023.4.0 y casi todos mis dispositivos IoT han dejado de enviar datos.

La investigación ha sido interesante. La cosa ha ido así:

  • Dado que el corte afecta a varios de mis dispositivos IoT a la vez y que coincide con la actualización de mis zonas SmartOS, asumo que está relacionado. Es evidente que hay un problema de compatibilidad.

  • El corte afecta a mis dispositivos IoT más antiguos, basados en ESP8266. Este chip IoT tiene muy poquita memoria RAM y soy consciente de que su soporte de TLS es mínimo. De hecho ya he tenido que hacer chapucillas en el pasado, como trocear un fichero grande en fragmentos pequeños para que fuese capaz de descargarlo.

  • Elegí uno de mis dispositivos IoT fácilmente accesible y lo conecté a mi portátil. Todos ellos van dando información por el puerto serie y muchos hasta ofrecen una consola interactiva. La conexión desde el portátil es simple:

    jcea@jcea:~$ screen /dev/ttyUSB0 115200
    
  • Ahí se ve lo siguiente:

    NodeMCU custom build by frightanic.com
            branch: dev
            commit: a33f5868551000d6f8e06def540bc5755b2146f0
            SSL: true
            modules: adc,am2320,bme280,cjson,crypto,dht,encoder,file,gpio,http,hx711,i2c,mdns,net,node,ow,pwm,rc,rtcfifo,rtcmem,rtctime,sigma_delta,sntp,spi,struct,tmr,uart,wifi
     build  built on: 2016-04-12 01:35
     powered by Lua 5.1.4 on SDK 1.5.1(e67da894)
    > CONECTADO
    -1      0       15.3    72.5
    -1      902451  1451    71824
    -1      0       15.3    73.3
    -1      902457  1451    71683
    -1      0       15.3    73.3
    -1      902469  1452    71642
    -1      0       15.3    73.3
    -1      902483  1453    71613
    

    El CONECTADO indica que se conecta a la WIFI correctamente. Las líneas siguientes son intentos de mandar datos al servidor, pero el -1 al principio de cada línea muestra que no lo consigue. Da un error.

  • Dado que este dispositivo IoT nos ofrece un terminal interactivo, intento hacer la conexión a mano al servidor SmartOS y, efectivamente, me sale un error. En el servidor no se ve ningún tipo de acceso en los registros, pero un sniffer sí muestra tráfico de red. Parece evidente que la hipótesis es correcta: mis instalaciones IoT antiguas son incapaces de negociar TLS con mis zonas SmartOS modernas, recién actualizadas.

  • Tengo una zona SmartOS que aún no he actualizado y que contiene un servidor web. Mira qué conveniente, qué bien me viene para las pruebas. Intento una conexión a ese servicio a través de la consola interactiva de mi dispositivo IoT y, como esperaba, establece la conexión TLS sin problemas:

    > http.get("https://X.jcea.es/", nil, function(code, data) print(code, data);end)
    401   <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
    <html><head>
    <title>401 Unauthorized</title>
    </head><body>
    <h1>Unauthorized</h1>
    <p>This server could not verify that you
    are authorized to access the document
    requested.  Either you supplied the wrong
    credentials (e.g., bad password), or your
    browser doesn't understand how to supply
    the credentials required.</p>
    </body></html>
    

    Aquí vemos que el dispositivo IoT conecta correctamente, aunque el servidor exige autenticación y nos rechaza con un error 401. Pero para llegar hasta ahí, el IoT y el servidor deben haber negociado primero una conexión TLS exitosa.

  • Por suerte, en los registros de mis servidores web me gusta guardar los detalles criptográficos de la conexión TLS. En este caso veo lo siguiente:

    TLSv1.1 AES128-SHA
    

    Ajá, el IoT está negociando una conexión TLS 1.1. Este protocolo TLS es muy antiguo y está desaconsejado.

  • TLS 1.1 se publicó en 2006. Este protocolo tiene numerosos defectos y, de hecho, TLS 1.2 salió poco después, en 2008 (dos años son poco tiempo para revisar un estándar).

    Dado que su reemplazo (TLS 1.2, 2008) lleva mucho tiempo en el mercado y es ubicuo y que TLS 1.1 y anteriores son vestigiales y contienen vulnerabilidades conocidas, hace tiempo que se desaconseja utilizar una versión de TLS anterior a la 1.2. Es la buena práctica actual. Tanto es así que en 2021 se publicó un RFC, el RFC 8996, con un título bastante evidente: Deprecating TLS 1.0 and TLS 1.1.

  • Dado todo lo anterior, tenemos varias posibilidades:

    • Podemos cambiar el DNS del servicio de recogida de datos IoT para que apunte a un servidor compatible con TLS 1.1. Por ejemplo, esto puede ser una Raspberry PI en casa, algo factible porque tengo la casa llena de ellas y dar un servicio más no es problema. Ese servicio podría recoger los datos con TLS 1.1 y enviarlos al servidor SmartOS de verdad a través de una conexión TCP usando una versión moderna de TLS.

      Este enfoque tiene la ventaja de que no tengo que tocar nada en mis dispositivos IoT, pero añade una dependencia extra en mi casa. Un servicio más a mantener. Sería fácil, pero no es elegante.

    • Otra posibilidad es configurar el servidor web para que vuelva a aceptar conexiones TLS 1.1. Esto sería aparentemente lo más trivial. Para no comprometer la seguridad TLS de todas las conexiones, lo ideal sería limitar esa compatibilidad exclusivamente al servidor web virtual de mi servicio IoT.

      Esto último puede no ser trivial [1] y requerir, por ejemplo, utilizar un puerto TCP diferente para el servicio IoT en el servidor web. Si este fuera el caso, haría falta modificar también los dispositivos IoT para que usasen el nuevo puerto TCP. El cambio sería simple, pero no transparente en los dispositivos IoT: mantener el software, pero cambiar la URL de conexión al servicio IoT.

      [1]

      Mi servidor web permite definir configuraciones TLS diferentes en cada servidor web virtual, pero es necesario que el cliente utilice SNI. Ni que decir tiene que algunos de mis dispositivos IoT no envían SNI. Y si hay uno que no lo hace, ya no puedo utilizarlo.

    • Una tercera posibilidad sería actualizar mis dispositivos IoT para manejar versiones modernas del protocolo TLS. Esto es factible aunque trabajoso, porque tengo muchos de estos cacharros funcionando que tendría que actualizar y porque el ESP8266 que uso en muchos de ellos es realmente anémico en recursos. Por suerte, compruebo que las versiones actuales de Lua para ESP8266 han logrado encajar ahí dentro TLS 1.2. Tiene mérito.

      Esto no supone un pequeño cambio en mi software en los dispositivos IoT como sería el punto anterior, sino que hay que actualizar todo el firmware, con consecuencias imprevisibles. Por ejemplo [2]:

      • El nuevo firmware puede consumir más RAM y puedo no tener sitio suficiente para mi software.
      • El manejo de sensores y actuadores puede ser diferente con el nuevo firmware y puede ser necesario modificar mi código.
      • Cambios en Lua podrían suponer diferencias de funcionamiento e incompatibilidades en mi código.

      Es decir, el proceso es factible, pero trabajoso y arriesgado.

      [2]

      Ni que decir tiene que eso fue exactamente lo que ocurrió.

  • Así que configuro un nuevo puerto TCP en mi servidor web para ser compatible con TLS 1.1. Esto no me ha funcionado tal cual, así que desactivo el soporte de TLS 1.2 y 1.3, por si la negociación de la versión del protocolo me está ocasionando problemas [3].

    [3]

    Si mi servidor web ofrece TLS 1.2 y 1.3 y mi dispositivo IoT es compatible con TLS 1.2, ambos deberían poder negociar y decidir usar TLS 1.2, sin más historias. Pero eso no es lo que ocurre en mi caso, la implementación TLS de mi dispositivo IoT falla una negociación que debería funcionar.

    Obviamente es un bug a resolver, debido posiblemente a cuando se escribió ese código no existía TLS 1.3 y no se pudo probar y, además, TLS 1.3 cambió su esquema de negociación para "abusar" de la negociación TLS 1.2 por compatibilidad con dispositivos TLS 1.2 que cascaban con el nuevo esquema de negociación TLS 1.3... perfectamente especificado y compatible si todo estuviese bien hecho [4].

    Que fallen este tipo de cosas es triste, pero es común. Se programa algo esperando cierta plantilla de datos y si llega algo que formalmente cumple el protocolo y debería funcionar, pero que no encaja con la plantilla que vemos siempre, pues falla. Es un problema de vaguería y validación de comportamiento, pero es tristemente común y pone en peligro la evolución de un protocolo aunque use correctamente los mecanismos de extensión previstos, porque siempre habrá alguien, a veces importante e influyente, que no hace la negociación como debe hacerse.

    Esta idea se denomina osificación y es algo que los ingenieros trabajando en nuevos estándares procuran tener en cuenta para obligar a los implementadores a que hagan las cosas bien. Por ejemplo, si un protocolo permite que varios campos se manden en cualquier orden, hacerlo así en vez de mandarlos siempre en el mismo orden para evitar que alguien sea vago y espere ese orden cuando el estándar no lo garantiza y es legal enviarlo de otra manera. Para que los programadores no se duerman y hagan chapuzas, vaya.

    [4]

    Ver, por ejemplo https://blog.cloudflare.com/why-tls-1-3-isnt-in-browsers-yet/.

    Desactivando esa negociación la cosa funciona, juzgando por el error que me da Firefox cuando intento conectarme al servicio:

    Secure Connection Failed
    
    An error occurred during a connection to X.jcea.es:xxxxx. Peer reports incompatible or unsupported protocol version.
    
    Error code: SSL_ERROR_PROTOCOL_VERSION_ALERT
    
    * The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.
    * Please contact the website owners to inform them of this problem.
    
    This website might not support the TLS 1.2 protocol, which is the minimum version supported by Firefox.
    

    Simplificando: Las versiones modernas de Firefox exigen TLS 1.2 como mínimo, y ese servicio web es TLS 1.1. Justo lo que queríamos comprobar.

  • La cosa tiene buena pinta... pero no funciona. Diría que la biblioteca TLS de las versiones modernas de SmartOS simplemente no soporta versiones antiguas de TLS. Aparentemente, seguir usando TLS 1.1 no es una opción sin esfuerzos heroicos.

    Mi gozo en un pozo :-(.

  • Como dispongo de muchos ESP8266 por casa, sacrifico uno para instalarle una versión moderna de Lua, con soporte de TLS 1.2 (previa compilación adhoc... que igual cuento otro día), para hacer pruebas. Lo que sale en el puerto serie es lo siguiente:

    NodeMCU 3.0.0.0 built with Docker provided by frightanic.com
            branch: release
            commit: 36cbf9f017d356319a6369e299765eedff191154
            release: 3.0.0-release_20211229 +25
            release DTS: 202402250804
            SSL: true
            build type: float
            LFS: 0x0 bytes total capacity
            modules: adc,am2320,bit,bme280,bme280_math,crypto,dht,encoder,file,gpio,http,hx711,i2c,mdns,mqtt,net,node,ow,pcm,pwm,rtcfifo,rtcmem,rtctime,sigma_delta,sjson,sntp,spi,struct,tls,tmr,uart,wifi
     build 2024-03-01 03:53 powered by Lua 5.1.4 on SDK 3.0.1-dev(fce080e)
    cannot open init.lua:
    >
    
  • Intento conectar a varios servicios web de prueba, aprovechando la interfaz interactiva del dispositivo IoT, y tengo el siguiente resultado:

    • Conecto con la zona SmartOS sin actualizar que referencio más arriba. En los registros del servidor web sale lo siguiente:

      TLSv1.2 ECDHE-RSA-CHACHA20-POLY1305
      

      Se ve que se negocia TLS 1.2 y un cifrado moderno. La actualización del firmware tiene buena pinta.

    • Conecto con el servicio web que me daba problemas, la zona SmartOS actualizada.

      No funciona.

      Aparentemente la negociación TLS, cuyo fin es determinar una versión común entre el cliente y el servidor lo más alta posible, no tiene éxito. Eso es porque cuando no hay compatibilidad con versiones TLS vetustas, la negociación de TLS 1.3 se disfraza de TLS 1.2 para mejorar su compatibilidad con dispositivos antiguos, pero a mis IoT no les gusta el proceso. Quieren TLS 1.2 sin sorpresas inesperadas.

      Es decir, negociar el protocolo TLS es un problema para estos dispositivos tan antiguos y tan escasos de recursos.

    • Conecto en un puerto TCP nuevo, configurado con TLS 1.2 explícito, sin negociación. ¡Funciona!.

      En los registros de mi servidor aparece lo siguiente:

      TLSv1.2 ECDHE-RSA-CHACHA20-POLY1305
      

      Perfecto.

  • La configuración de mi servidor web (Apache HTTP) queda así:

    <VirtualHost *:PUERTO>
    ServerName NOMBRE.jcea.es
    
    SSLEngine on
    SSLProxyEngine on
    
    SSLProtocol +TLSv1.2 -TLSv1.3
    
    ErrorLog logs/NOMBRE_error.log
    CustomLog logs/NOMBRE_access.log combinedSSL
    
    ProxyPass / https://NOMBRE.jcea.es/
    </VirtualHost>
    

    Lo que hago es definir un nuevo servidor web virtual en un puerto TCP separado que no negocia TLS 1.3 (es decir, solo admite TLS 1.2). Todo lo que llega ahí se envía al servidor web original en el puerto TCP normal, con TLS moderno. En otras palabras, ese servidor web virtual actúa como proxy inverso.

    Si enviamos nuestros dispositivos TLS a ese nuevo servidor web virtual, seguiremos funcionando correctamente mientras el servidor SmartOS soporte TLS 1.2, aunque en el servidor web general estemos utilizando una versión más avanzada (y segura) de TLS.

    Advertencia

    Dado que ahora al servidor web IoT "de verdad" le van a entrar conexiones desde la dirección IP 127.0.0.1 (las provenientes del servidor web IoT "adaptador" que se ejecuta en el mismo servidor), es importante que la dirección IP 127.0.0.1 no tenga privilegios especiales de autenticación y/o autorización en el servidor IoT.

  • Una vez que tenemos un firmware actualizado con soporte TLS 1.2 y comprobamos que el ESP8266 ya puede conectarse correctamente a mi servidor SmartOS, instalo mi software IoT personalizado y asunto solucionado hasta dentro de otros diez años... espero.

    Ya solo queda repasar toda mi casa y actualizar casi una docena de cacharros escondidos en lugares recónditos, algunos bastante mugrientos.

Me hace gracia pensar que todo este proceso, aparentemente molesto y laborioso, fue tarea de una tarde en un día frío y lluvioso en que me cancelaron mis planes de fin de semana. Fue medio sábado bien aprovechado...