Verificar contenido DNS con DNSSEC y Python

Ahora que tengo todas las piezas DNSSEC en su lugar (dominios protegidos por DNSSEC y clientes que verifican DNSSEC), queda explicar cómo lo estoy utilizando.

Conviene leer el contexto primero.

Teniendo DNSSEC, tenemos una base de datos masivamente distribuída y masivamente cacheada. Podemos atender millones de dispositivos de forma segura sin apenas carga en nuestros servidores DNS. Algunos ejemplos de uso:

En mi caso, estoy utilizando esta tecnología para controlar y actualizar una miriada de dispositivos IoT y sistemas empotrados que tengo distribuídos por todo el mundo. El DNS me permite hacerlo de una forma escalable y redundante (gracias al cacheo de la red mundial de servidores DNS) y DNSSEC me permite hacerlo de una forma segura.

¿Cómo? Veamos el siguiente código Python. Utiliza la biblioteca dnspython:

 #!/usr/bin/env python3

 # (c) 2016-2018 jcea@jcea.es - https://www.jcea.es/
 # Código liberado como Dominio Público.
 # Haz con él lo que quieras.


 import dns.query
 import dns.message
 import dns.rdatatype
 import dns.flags
 import dns.rcode


 def resuelve(nombre, tipo=dns.rdatatype.A, timeout=5):
     request = dns.message.make_query(f'{nombre}.', tipo,
                                      want_dnssec=True)
     r = dns.query.udp(request, '127.0.0.1', timeout=timeout)
     if r.rcode() != dns.rcode.NOERROR:
         error = dns.rcode.to_text(r.rcode())
         raise RuntimeError(f'La respuesta DNS indica un error {error}')
     if not (r.flags & dns.flags.QR):
         raise RuntimeError('Se recibe una respuesta que no es respuesta')
     if not (r.flags & dns.flags.AD):
         raise RuntimeError('La respuesta no está autenticada')

     return r.answer


 def hace_peticion(nombre):
     try:
         r = resuelve(nombre)
     except RuntimeError as exc:
         r = [exc.args[0]]
     else:
         r = [i.to_text() for i in r]

     for i in r:
         print(f'{nombre}: {i}')


 for i in ['www.jcea.es',
           'www.elpais.es',
           'este_dominio_no_existe.jpeg',
           'dnssec-failed.org',
           ]:
     hace_peticion(i)
     print()

Las líneas 42-48 hacen una serie de peticiones DNS a varios dominios. La función en las líneas 30-39 realiza las peticiones en sí y nos muestra los resultados y los errores. La función en las líneas 15-27 se encarga de la comunicación real con el servidor DNS y analiza la respuesta.

Para la consulta estamos usando el servidor DNS en la misma máquina [1] (la dirección IP 127.0.0.1) y le estamos delegando toda la verificación DNSSEC. Podemos usar dnsmasq, BIND o similares, tal y como se ha explicado con anterioridad en artículos como Activar verificación de DNSSEC en "dnsmasq".

Técnicamente la biblioteca dnspython tiene todo lo necesario para realizar la verificación DNSSEC completa, extremo a extremo y sin depender de otro proceso local, pero dado que ya estamos usando un resolver local con verificación DNSSEC para el resto actividades del sistema operativo, no parece valer la pena complicarse más la vida.

[1] (1, 2)

Esto tiene una importancia fundamental. Este programa podría delegar la petición DNS a un servidor externo con capacidad DNSSEC, como el 8.8.8.8 de Google, pero eso tiene dos problemas:

  1. Estamos delegando nuestra seguridad en una tercera parte que no solo no tiene interés en protegernos, sino que puede ser activamente hostil.
  2. Aunque el servidor DNS al que consultemos sea de fiar, su respuesta no está protegida y atraviesa multitud de redes hasta llegar a nosotros, incluyendo elementos hostiles en nuestra propia red local. Pensemos en gobiernos enemigos, operadores de telecomunicaciones deseosos de monetizarnos o "hackers" en la red del hotel o el restaurante.

Si la verificación se realiza en nuestro propio ordenador, no es necesario confiar en nadie más. El sistema local detectará cualquier manipulación, venga de quien venga.

Si ejecutamos este código en una máquina con un resolver local capaz de gestionar DNSSEC, obtendremos un resultado similar al siguiente:

www.jcea.es: www.jcea.es. 12238 IN RRSIG A 7 3 28800 20180321112035 ...
www.jcea.es: www.jcea.es. 12238 IN A 176.9.11.11

www.elpais.es: La respuesta no está autenticada

este_dominio_no_existe.jpeg: La respuesta DNS indica un error NXDOMAIN

dnssec-failed.org: La respuesta DNS indica un error SERVFAIL

Vemos lo siguiente:

  • El dominio www.jcea.es está protegido por DNSSEC y la verificación es correcta. Obtenemos la dirección que hemos pedido y una firma digital que no nos interesa porque hemos delegado su verificación en el resolver local [1].

    Estamos seguros de que esa información es correcta y de confianza.

  • El dominio www.elpais.es nos da una respuesta, pero no está protegida por DNSSEC y, por tanto, no sabemos si podemos fiarnos o no de ella. En mi sistema IoT, sencillamente, no me fío.

  • El dominio inventado este_dominio_no_existe.jpeg claramente no existe, así se nos indica con un error NXDOMAIN. De hecho ese error lleva una firma DNSSEC que se puede comprobar, de forma que sabemos seguro que esa "no existencia" del dominio es demostrable y nadie la está falsificando:

    $ dig este_dominio_no_existe.jpeg
    
        ; <<>> DiG 9.10.3-P4-Raspbian <<>> este_dominio_no_existe.jpeg
        ;; global options: +cmd
        ;; Got answer:
        ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 64399
        ;; flags: qr rd ra ad; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
    

    El error status: NXDOMAIN nos indica que el dominio no existe, y el flag ad (authenticated data) nos indica que este mensaje está firmado correctamente con DNSSEC, certificando su autenticidad.

  • Por último, el dominio dnssec-failed.org es un dominio DNSSEC cuya verificación siempre falla y que se emplea para comprobar el correcto funcionamiento de la seguridad. Ofrezco algunos detalles en Activar verificación de DNSSEC en "dnsmasq".

Naturalmente, el código Python real es bastante más sofisticado, solo he mostrado un ejemplo de uso.