- Un desarrollador comparte el recorrido técnico y mental que vivió al implementar por su cuenta un compilador de ASN.1 (dasn1) en el lenguaje D
- El proyecto busca la implementación de x.509 y TLS 1.3, y soporta el complejo procesamiento de codificación DER de ASN.1
- El texto aborda en detalle la complejidad estructural de ASN.1, la dificultad de implementar las especificaciones x.680~x.683 y cómo aprovechar la metaprogramación de D
- Explica de forma concreta cómo funciones de D como
static import, mixin template, typeof(), alias this fueron útiles para la generación de código y el diseño de AST/IR
- El autor resume que “ASN.1 es doloroso, pero se aprende muchísimo”, y transmite con honestidad las dificultades reales y la recompensa de crear un compilador
Panorama general del proyecto y motivación
- El autor está desarrollando Juptune, un framework de E/S asíncrona basado en D, y para implementar TLS necesitaba procesar directamente la codificación DER de ASN.1
- Para parsear la estructura de los certificados x.509 de TLS, era necesario entender la compleja forma en que ASN.1 representa los datos
- Este proyecto comenzó como un reto personal por aprendizaje y diversión, y de hecho ya llegó a la etapa de parsear correctamente algunos certificados
- Aunque ASN.1 es un estándar antiguo de los años 90, todavía se usa ampliamente en sistemas modernos como TLS, SNMP y LDAP
- El autor menciona que “ASN.1 se usa por todas partes, pero la mayoría de los desarrolladores ni siquiera sabe que existe”
¿Qué es ASN.1?
- ASN.1 (Abstract Syntax Notation One) es un lenguaje para definir y codificar estructuras de datos, una especie de “antepasado de Protocol Buffers”
- El estándar se compone de la notación (x.680~x.683) y las reglas de codificación (BER, CER, DER, PER, XER, JER, etc.)
- BER: formato TLV básico, con soporte para longitud indefinida
- CER: variante restringida de BER, que siempre usa longitud indefinida
- DER: subconjunto determinista de BER, usado como estándar en criptografía
- PER/OER: codificación comprimida a nivel de bits
- XER/JER: codificación basada en XML y JSON
- La gran variedad de codificaciones lo vuelve complejo, pero también le da alta flexibilidad y extensibilidad
La complejidad de la notación ASN.1
- El estándar base de ASN.1 es x.680, y las especificaciones extendidas (x.681~x.683) están escritas con un estilo académico extremadamente difícil de leer
- Se puede implementar solo con x.680, pero la gran cantidad de reglas de transformación semántica y variaciones sintácticas hace que la implementación sea complicada
- x.681 define el sistema de Information Object Class y admite una sintaxis propia de inicialización
- Ejemplo:
CALLED &name [WHO IS &age YEARS OLD]
- x.682 define Table Constraint, y x.683 define tipos parametrizados
- Es un concepto parecido a los genéricos de D, ya que puede recibir tanto tipos como valores como parámetros
Funciones interesantes de ASN.1
- Sistema de restricciones (Constraint): permite indicar directamente el rango o tamaño de los valores al definir un tipo
- Ejemplo:
UInt8 ::= INTEGER (0..255)
- Soporta operadores
SIZE, UNION(|), INTERSECTION(^)
- Sistema de versionado: mediante
OBJECT IDENTIFIER se pueden distinguir con claridad las versiones de los módulos
- Ejemplo:
id-pkix1-implicit(19) vs id-mod-pkix1-implicit-02(59)
- Permite identificar módulos con claridad sin choques de nombres
Por qué el lenguaje D favorece la generación de código
- El
static import de D evita conflictos de nombres y permite conservar tal cual los nombres de tipos de ASN.1
- La búsqueda local al módulo mediante
.Type1 limita con claridad la resolución de símbolos
- Con
typeof() se puede inferir automáticamente el tipo, lo que evita tener que administrarlo manualmente al generar código
- El soporte de coma final (
trailing comma) simplifica la generación de código
- Gracias a la composición de constantes en tiempo de compilación, es posible combinar cadenas incluso dentro de funciones
@nogc
Casos de implementación aprovechando funciones de D
Nodos AST basados en mixin templates
- Usó la función
mixin template de D para definir nodos del árbol sintáctico de ASN.1 (AST)
- Cada tipo de nodo (
List, Container, OneOf) se reutiliza como plantilla
- En lugar de una herencia compleja, se simplifica mediante copia de código en tiempo de compilación
API basada en templates y validación en tiempo de compilación
- El nodo
Container incluye varios subnodos y realiza validación de tipos en tiempo de compilación
- Se puede acceder de forma segura con expresiones como
node.getNode!Asn1TagDefaultNode
- El nodo
OneOf almacena uno entre varios tipos y soporta pattern matching con la función match
- Como obliga a definir manejadores para todos los tipos, garantiza seguridad en tiempo de compilación
Uso del paquete experimental de administración de memoria de D
- Con
std.experimental.allocator implementó creación y liberación de objetos en un entorno @nogc
- Combinó componentes como
Region y StatsCollector para construir asignadores personalizados
- Eso sí, sigue siendo experimental incluso después de 10 años
Función alias this
- Mediante
alias this, hizo que structs contenedores se comporten como si fueran su campo interno
- Ejemplo: permite casts concisos como
cast(Asn1ValueReferenceIr)item
version(unittest)
- La palabra clave
version(unittest) permite definir funciones exclusivas para pruebas, que no se incluyen en el build real
Harness de pruebas con templates + with()
- La lógica común de pruebas se parametriza con templates y, usando la sentencia
with(), se logra escribir pruebas más concisas
- Se puede llamar
T() en lugar de Harness.T()
Principales dificultades durante la implementación
Sintaxis de secuencia de valores (Value Sequence Syntax)
- Varias formas de sintaxis de valores que empiezan con
{} son ambiguas según el contexto
- El parser incluso tiene un comentario del tipo “esto no es divertido”, reflejando lo enredado del asunto
- Como separó el análisis sintáctico del análisis semántico, la dificultad de procesarlo aumentó
Falta de claridad en la especificación
- Existen comportamientos no indicados explícitamente en la documentación, como reglas donde cierto tag debe tratarse como
EXPLICIT bajo determinadas condiciones
- Tampoco está claramente definido el modo de gestionar versiones de módulos
Necesidad de implementar las restricciones por triplicado
- Para validación sintáctica
- Para validación de valores
- Para generación de código en tiempo de ejecución
- Al manejar
UNION e INTERSECTION, también se vuelve compleja la construcción de mensajes de error
La ilusión de nodos IR inmutables
- Pensó que después de convertir el AST a IR ya no haría falta modificar nada,
pero procesos de transformación semántica como AUTOMATIC TAGS exigieron cambiar los datos
La complejidad total de ASN.1
- x.509 usa solo una sintaxis antigua y por eso es relativamente simple, pero los estándares modernos requieren implementar x.681~x.683
- Por eso ASN.1 casi no se usa fuera de ámbitos académicos o comerciales muy específicos
El problema de ANY DEFINED BY
ANY DEFINED BY es una estructura cuyo tipo cambia según el valor de otro campo
- dasn1 no lo implementa y lo reemplaza por un intrínseco personalizado
Dasn1-Any
- Al decodificar de verdad, hace falta manejarlo manualmente
Sobrecarga de información
- Al llevar en paralelo ASN.1, x.68x, x.690, Juptune y otros frentes, era difícil mantener el contexto del codebase
La realidad de crear un compilador
- Miles de visitantes de nodos, código repetitivo e implementaciones con diferencias mínimas hacen que sea un trabajo tedioso y agotador
- Aun así, en cada etapa hubo una gran sensación de logro y aprendizaje
- El autor recuerda que “probablemente nadie lo use, pero sí obtuve experiencia real construyendo un compilador”
- Cierra el texto con la broma: “no hagan ASN.1, te cambia la vida”
Conclusión
- A pesar de un año de trabajo, dasn1 sigue incompleto, pero fue una oportunidad para entender a fondo el potencial del lenguaje D y la complejidad de ASN.1
- Con la esperanza de algún día poder poner en su CV “experiencia implementando un compilador de ASN.1 + TLS 1.3”,
el texto concluye repasando con humor el crecimiento del desarrollador y la realidad de la industria
1 comentarios
Comentarios de Hacker News
En resumen, quería hablar de ASN.1, del lenguaje D y del compilador en sí
Pero como no encontraba un formato consistente, junté esas ideas relacionadas en una entrada de blog
No está del todo pulido, pero es un tema difícil de tratar brevemente, así que espero comprensión
Matemáticamente,
{0} ∪ ({2} ∩ {4,5,6,7,8}) = {0}, así que al final solo se permite un único valorEn lo personal me gusta muchísimo D, pero siendo realistas, Go y Rust se usan mucho más
Entiendo profundamente el sufrimiento del autor
Amo D, pero lo he tenido abandonado desde hace tiempo
Como antes trabajé con parsers e implementación de protocolos, me resultó todavía más interesante
“OMG ASN.1”, qué tema tan agradable de ver
Recuerdo la época en que internet estaba creciendo y la IETF hacía avanzar los protocolos
En ese entonces a las empresas no les interesaba internet, y el liderazgo lo llevaban la academia y la IETF
Pero cuando las empresas se dieron cuenta de que había dinero ahí, empezó la Protocol Wars
ASN.1 es producto de esa guerra y un ejemplo del choque entre la cultura corporativa y la cultura académica
Se podría decir que las empresas representan una “cultura de recetas” y la academia una “cultura de funciones”
Esa diferencia de mentalidad también ofrece pistas sobre la cultura de desarrollo de IA actual
Pensar que podríamos haber terminado con un sistema de direcciones como “CN=wikipedia, OU=org, C=US” en lugar de internet da escalofríos
En realidad, los protagonistas eran ITU e ISO
Después, a fines de los 90, hubo otra “guerra de protocolos”, y esta vez la IETF perdió
ISO buscaba la perfección y por eso se movía lento, mientras que la IETF avanzaba rápido con la actitud de “ya lo arreglamos después”
Como resultado, surgió el problema de que los protocolos quedaran endurecidos demasiado pronto
También fue un problema que las implementaciones ASN.1 para C en los años 90 fueran pésimas
Hay un dicho turco que dice: “¡Esto no es algo para que lo use una persona!”
Me gustaría adoptarlo como lema de filosofía de diseño
Y, como en la frase de Game of Thrones de que “quien dicta la sentencia debe blandir la espada”,
quien escribe la especificación debería implementar personalmente el parser
Si una especificación solo pudiera aprobarse junto con un parser funcional y sus pruebas, la calidad probablemente mejoraría muchísimo
Me encanta el lenguaje D
Estoy implementando por mi cuenta un editor de texto estilo vim que solo depende de Raylib
Las ventajas de D son las siguientes
version(unittest)es fácil manejar código exclusivo para pruebasSiempre que consultaba la documentación o le preguntaba a ChatGPT, terminaba encontrando una solución elegante
Desde la filosofía de diseño está cerca de ser perfecto, pero si sus herramientas y ecosistema estuvieran al nivel de Rust o Go, habría tenido mucho más éxito
La biblioteca estándar Phobos tiene demasiadas pequeñas incomodidades y al final la abandoné
La nueva versión, Phobos V3, está en marcha, pero como hay poca gente trabajando en ella, tengo esperanza y preocupación a la vez
“¿Cuándo dije que ASN.1 fuera complejo?”
Tanto el esquema como el formato de datos son complejos, pero la mayor parte de esa complejidad puede ignorarse
Yo no usé la notación de esquemas ASN.1, sino que escribí directamente una implementación de DER en C
DER me parece la única codificación estándar que realmente vale la pena usar
Además, también hice formatos de codificación propios como DSER, SDSER y TER
Estructuras como
ANY DEFINED BYsiguen siendo útiles,y para una codificación eficiente incluso agregué una función no estándar llamada OBJECT IDENTIFIER RELATIVE TO
Yo también he hecho un compilador ASN.1
Aunque solo implementé parte de X.681 a X.683, logré que con una sola llamada al códec se pudiera decodificar recursivamente un certificado completo
ASN.1 no es solo una gramática simple, sino un sistema de tipos potente
Está subestimado, pero de verdad es una tecnología increíble
Hace tiempo hice un compilador ASN.1 para Swift
En el proyecto ASN1Codable, aprovechando libasn1 de Heimdal,
convertí ASN.1 a un JSON AST para simplificar el parsing
Eso de “convirtámoslo a JSON” suena, al final, como el grito de un desarrollador herido 😄
Curiosamente, trabajar con ASN.1 se siente divertido
Algún día me gustaría hacer mi propio compilador ASN.1 para Rust
Las implementaciones actuales en Rust en su mayoría dependen de macros derive o de encadenamiento manual, y eso me deja con ganas de más
En general, al implementar un estándar se completa el 80% de las funciones en el 20% del tiempo,
pero el 20% restante de ASN.1 podría tomar toda una vida
Hace tiempo amplié el parser ASN.1 del código base de Netscape para dar soporte a PKCS#12
Me arrepentí de haber aprendido demasiado a fondo el estándar RSA y las definiciones ASN.1,
pero le tengo respeto a la persistencia y un poco de masoquismo del autor del blog