Cuando una zona DNS está firmada con DNSSEC, a cada registro (RR) en la zona, se le agregan uno o más registros con las firmas digitales (RRSIG). Cada RRSIG tiene una fecha de inserción (inception) y una fecha de caducidad (expiration). Cuando un DNS de resolución (resolver) consulta la zona para obtener los RR y solicita firmas DNSSEC, una de las cosas que hace es verificar que los RRSIG estén vigentes.

Es posible verificar los RRSIG para un RR particular de forma manual, ejecutando

$ dig +short +dnssec a iamemhn.link
107.20.149.94
A 8 2 86400 20200505151009 20200405143739 9123 iamemhn.link. yaPrQpXR47XGfziAXlqfTV/s3ZkahbpafgZFZWQUaterIgW2/uwcRhi5 nKCiTtdIIvrKyvY35yhETxxHlvYZtPheF1dv9nwlpSoo5J7GzbIlrhwL 5axRaxX7BRu/sqCJ7HwGWOodEhv2h4IQ248WA84j2LiNjsTQ7nRK0Fpm UvA=

En este ejemplo, consultamos el RR A (address) asociado al nombre iamemhn.link, esperando una respuesta breve firmada con DNSSEC.

La primera línea de la respuesta es, en efecto, la información asociada al RR, que podríamos obtener sin DNSSEC. La segunda línea (bastante larga) es el RRSIG que acompaña al RR A y que lo certifica digitalmente según DNSSEC.

La fecha de expiración es el quinto campo (2020-05-05Z15:10:09) y la fecha de inserción es el sexto campo (2020-04-05Z14:37:39). El resto indica el algoritmo criptográfico en uso, el tipo de suma digital (hash), así como el identificador de la llave que firma el registro.

Los servidores DNS modernos como BIND9 permiten configurar mecanismos automáticos para asegurar que se inserten nuevos RRSIG regularmente, evitando la presencia de RRSIG inválidos y así mantener la zona resolviendo correctamente con DNSSEC. Sin embargo, instrumentación que permita determinar si hay RRSIG próximos a vencer forma parte de una operación DNSSEC robusta.

Pensando en eso, escribí un utilitario de línea de comandos en Haskell que asiste en la verificación de todos los RRSIG para todos los RR disponibles, de uno o más nombres. La intención es que sirva de auxiliar a herramientas de monitoreo y supervisión de redes, emitiendo su salida como un reporte CSV o bien como un objeto JSON.

Ahora puedo hacer

$ check-dnssec-signatures -j -d 21 iamemhn.link
{
    "getResults": {
        "iamemhn.link": [
            {
                "inception": 1586100546,
                "expiration": 1588694932,
                "alert": false,
                "rrType": "DNSKEY"
            },
            {
                "inception": 1586100546,
                "expiration": 1588694932,
                "alert": false,
                "rrType": "DNSKEY"
            },
            {
                "inception": 1586102683,
                "expiration": 1588698283,
                "alert": false,
                "rrType": "SOA"
            },
            {
                "inception": 1585788478,
                "expiration": 1588382264,
                "alert": true,
                "rrType": "NSEC"
            },
            {
                "inception": 1586097459,
                "expiration": 1588691409,
                "alert": false,
                "rrType": "MX"
            },
            {
                "inception": 1586097459,
                "expiration": 1588691409,
                "alert": false,
                "rrType": "TXT"
            },
            {
                "inception": 1586097459,
                "expiration": 1588691409,
                "alert": false,
                "rrType": "NS"
            },
            {
                "inception": 1586097459,
                "expiration": 1588691409,
                "alert": false,
                "rrType": "A"
            }
        ]
    }
}

y tener todos los RR disponibles para el nombre, junto con sus fechas de inserción y expiración, así como un indicador de cuál de ellos expira en los próximos 21 días.

Al mismo tiempo, me pareció un proyecto suficientemente pequeño y razonablemente corto (aproximadamente dos horas desde stack init hasta git push) que ilustra la flexibilidad y robustez de Haskell para escribir programas de sistema:

  • Opciones de línea de comando con optparse-applicative: aunque sean pocas, son procesadas por un reconocedor formal de tipos fuertes en lugar de usar cadenas y expresiones regulares. Al mismo tiempo, construirlo usando combinadores aplicativos y componentes monoidales es muy conveniente.

  • DNS: la librería dns es sumamente robusta y simple de usar en los casos frecuentes. Este ejercicio muestra como usarla en un escenario que no es el más frecuente, y aún así se puede llegar hasta el nivel más bajo de representación de los mensajes DNS sin perder la protección del sistema de tipos. Tanto así, que hay algunos tipos de RR que no están soportados «directamente» pero fueron procesados de forma genérica sin problemas.

  • Concurrencia: la resolución DNS suele tener latencia imprevisible, y por eso este utilitario procesa cada nombre en un hilo independiente. Aproveché la librería async para procesar una lista de longitud arbitraria concurrentemente, sin necesidad de escribir fork, usar co-rutinas, ni establecer mecanismos de sincronización con semáforos o canales manualmente.

  • Generación automática de serializadores JSON: la librería aeson es muy práctica pues en compañía de las facilidades de programación genérica de GHC, es capaz de generar reconocedores (no los necesité) y serializadores JSON con tipos fuertes en muy pocas líneas de código. Y encima, con pretty-printing

  • Programación genérica: para poder explotar la concurrencia aproveché el sistema de tipos de Haskell y sus typeclasses para expresar operaciones implícitas. Indiqué la manera de combinar diccionarios para producir nuevos diccionarios (Monoid) y así poder expresar la combinación implícita de los resultados parciales recogidos concurrentemente. Encima, es el tipo de técnicas que permiten a GHC generar código extremadamente eficiente -- mucho más que si programara con recursión explícita.

  • Generación asistida de serializadores CSV: la librería cassava tiene características similares que aeson, pero en relación a reconocedores (no los necesité) y serializadores CSV con tipos fuertes. En este caso, tuve que escribir un poco más de código. Como ocurre generalmente en Haskell, sólo tuve que expresar las restricciones en los tipos de datos involucrados y una breve transformación pura, y la librería generó el serializador.

  • «String es el tipo de datos del pobre». Aproveché bytestring en mi interacción con DNS y la construcción de serializadores, por su extremada velocidad gracias a las técnicas de fusión presentes en su interfaz que parece una lista, pero es más inteligente. Así mismo, aproveché text en mi interacción con el humano, tanto para leer argumentos como para emitir los resultados. Esto requiere codificar y fusionar entre ambos tipos, cosa que ocurre con mucha frecuencia en aplicaciones de sistemas. Al mismo tiempo, queda clara la «frontera de conversión, para que cada librería opere con el tipo de datos más eficiente para su trabajo.

  • Fechas y horas. El manejo de fechas y horas como tipos de datos que se comportan como números, pero que no se pueden mezclar con números por accidente.

Como siempre, una separación entre código puro y efectos en el mundo IO contribuyen a facilitar la refactorización e identificar el flujo de datos como una transformación global y no como manipulaciones del estado mutable.

La herramienta está aquí con instrucciones mínimas para compilar usando stack. Espero poder comentarla la próxima vez que asista a los Orange Combinators.