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 deRR
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 escribirfork
, 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-printingProgramació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 queaeson
, 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.