- Un développeur partage son parcours technique et mental après avoir implémenté lui-même un compilateur ASN.1 (dasn1) en langage D
- Le projet vise l’implémentation de x.509 et de TLS 1.3, avec prise en charge du traitement complexe de l’encodage DER d’ASN.1
- L’article détaille la difficulté structurelle d’ASN.1, la complexité d’implémentation des spécifications x.680 à x.683, ainsi que l’usage de la métaprogrammation en D
- Il explique concrètement comment des fonctionnalités de D comme static import, les mixin templates, typeof() et alias this ont été utiles pour la génération de code ainsi que la conception de l’AST et de l’IR
- L’auteur conclut qu’« ASN.1 est douloureux, mais très formateur », et décrit avec franchise les difficultés concrètes et les satisfactions liées à la création d’un compilateur
Vue d’ensemble du projet et motivations
- L’auteur développe actuellement Juptune, un framework d’E/S asynchrones basé sur D, et avait besoin de gérer lui-même l’encodage ASN.1 DER pour implémenter TLS
- Pour parser la structure des certificats x.509 de TLS, il fallait comprendre le mode de représentation complexe des données en ASN.1
- Ce projet a commencé comme un défi personnel, à la fois pour apprendre et pour le plaisir, et il a déjà permis de parser avec succès certains certificats
- ASN.1 est un ancien standard des années 1990, mais il reste encore utilisé dans de nombreux systèmes modernes comme TLS, SNMP ou LDAP
- L’auteur remarque qu’« ASN.1 est largement utilisé dans le monde, mais la plupart des développeurs ignorent jusqu’à son existence »
Qu’est-ce qu’ASN.1 ?
- ASN.1 (Abstract Syntax Notation One) est un langage pour définir et encoder des structures de données, en quelque sorte un « ancêtre de Protocol Buffers »
- Le standard se compose de la notation (x.680 à x.683) et des règles d’encodage (BER, CER, DER, PER, XER, JER, etc.)
- BER : format TLV de base, avec prise en charge des longueurs infinies
- CER : variante restreinte de BER, utilisant toujours des longueurs infinies
- DER : sous-ensemble déterministe de BER, utilisé comme standard en cryptographie
- PER/OER : encodage compressé au niveau du bit
- XER/JER : encodage basé sur XML et JSON
- Cette diversité d’encodages rend l’ensemble complexe, mais lui donne aussi une grande souplesse et extensibilité
La complexité de la notation ASN.1
- Le standard de base d’ASN.1 est x.680, et les spécifications d’extension (x.681 à x.683) sont rédigées dans un style académique particulièrement ardu
- Une implémentation est possible avec x.680 seul, mais les nombreuses règles de transformation sémantique et variantes syntaxiques en rendent l’implémentation difficile
- x.681 définit le système des Information Object Class et prend en charge une syntaxe d’initialisation spécifique
- Exemple :
CALLED &name [WHO IS &age YEARS OLD]
- x.682 définit les Table Constraint, et x.683 les types paramétrés
- Un concept proche des génériques en D, capable de recevoir à la fois des types et des valeurs comme paramètres
Des fonctionnalités ASN.1 intéressantes
- Système de contraintes : il permet de préciser directement, dans la définition d’un type, une plage de valeurs ou une taille
- Exemple :
UInt8 ::= INTEGER (0..255)
- Prise en charge des opérateurs
SIZE, UNION(|) et INTERSECTION(^)
- Système de gestion de version :
OBJECT IDENTIFIER permet de distinguer clairement les versions d’un module
- Exemple :
id-pkix1-implicit(19) vs id-mod-pkix1-implicit-02(59)
- Cela permet d’identifier clairement les modules sans conflit de nom
Pourquoi le langage D est avantageux pour la génération de code
- Le static import de D évite les conflits de noms et permet de conserver tels quels les noms de types ASN.1
- La recherche locale au module (
.Type1) permet de limiter explicitement la résolution des symboles
- typeof() permet d’inférer automatiquement les types, évitant une gestion manuelle lors de la génération de code
- L’autorisation de la virgule finale (trailing comma) simplifie la génération de code
- Grâce à la concaténation de constantes à la compilation, il est possible de composer des chaînes même dans des fonctions
@nogc
Exemples d’implémentation exploitant les fonctionnalités de D
Nœuds d’AST basés sur des mixin templates
- La fonctionnalité mixin template de D est utilisée pour définir les nœuds de l’arbre syntaxique ASN.1 (AST)
- Chaque type de nœud (
List, Container, OneOf) est réutilisé sous forme de template
- Cela simplifie le code par copie à la compilation, plutôt que via une hiérarchie d’héritage complexe
API à base de templates et vérification à la compilation
- Le nœud
Container contient plusieurs sous-nœuds et effectue une vérification des types à la compilation
- Un accès sûr est possible sous la forme
node.getNode!Asn1TagDefaultNode
- Le nœud
OneOf stocke l’un de plusieurs types et prend en charge le pattern matching via la fonction match
- Comme tous les handlers de type doivent être définis, cela garantit une sécurité à la compilation
Utilisation du package expérimental de gestion mémoire de D
std.experimental.allocator est utilisé pour créer et libérer des objets dans un environnement @nogc
- Un allocateur personnalisé est composé à partir d’éléments comme
Region et StatsCollector
- Mais le module reste à l’état expérimental depuis déjà dix ans
Fonctionnalité alias this
alias this est utilisé pour permettre à des structures wrapper de se comporter comme leur champ interne
- Exemple : un cast concis comme
cast(Asn1ValueReferenceIr)item
version(unittest)
- Le mot-clé
version(unittest) permet de définir des fonctions réservées aux tests, exclues du build final
Harness de test avec template + with()
- La logique de test commune est template-ifiée, et l’instruction
with() permet d’écrire un code de test concis
- On peut appeler
T() au lieu de Harness.T()
Principales difficultés rencontrées pendant l’implémentation
Syntaxe des séquences de valeurs (Value Sequence Syntax)
- Les multiples formes de syntaxe de valeur commençant par
{} sont ambiguës selon le contexte
- La complexité est telle qu’un commentaire dans le parseur dit en substance : « ce n’est pas amusant »
- Comme l’analyse syntaxique et l’analyse sémantique ont été séparées, le traitement est devenu encore plus difficile
Manque de clarté de la spécification
- Certaines règles, comme le fait qu’un tag doive être traité comme
EXPLICIT dans des cas particuliers, correspondent à des comportements non explicitement documentés
- Le mode de gestion des versions de module n’est pas non plus clairement défini
Nécessité de tripler l’implémentation des contraintes
- pour la validation syntaxique
- pour la validation des valeurs
- pour la génération de code à l’exécution
- La gestion de
UNION et INTERSECTION rend aussi la construction des messages d’erreur complexe
L’illusion des nœuds IR immuables
- L’auteur pensait qu’une fois l’AST converti en IR, aucune modification ne serait nécessaire,
mais certains processus de transformation sémantique comme AUTOMATIC TAGS exigent en réalité de modifier les données
La complexité omniprésente d’ASN.1
- x.509 n’utilise qu’une syntaxe ancienne et reste donc relativement simple, mais les spécifications récentes imposent d’implémenter x.681 à x.683
- C’est l’une des raisons pour lesquelles ASN.1 est très peu utilisé en dehors des domaines académiques ou commerciaux spécialisés
Le problème de ANY DEFINED BY
ANY DEFINED BY est une structure dont le type change selon la valeur d’un autre champ
- dasn1 ne l’implémente pas et le remplace par un intrinsic personnalisé
Dasn1-Any
- Un traitement manuel reste nécessaire lors du décodage réel
Surcharge d’informations
- Entre ASN.1, x.68x, x.690, Juptune et d’autres projets menés en parallèle, il était difficile de garder le contexte du codebase en tête
La réalité de la fabrication d’un compilateur
- Des milliers de visiteurs de nœuds, du code répétitif et des implémentations aux différences infimes : un travail long, pénible et souvent monotone
- Mais chaque étape apporte aussi un fort sentiment d’accomplissement et un réel apprentissage
- L’auteur revient sur l’expérience en disant : « personne ne l’utilisera probablement, mais j’ai acquis une vraie expérience de compilateur »
- Il conclut enfin sur une plaisanterie : « ne faites pas d’ASN.1, ça change une vie »
Conclusion
- Malgré une année de travail, dasn1 n’est pas encore terminé,
mais ce projet a permis à l’auteur de comprendre en profondeur le potentiel du langage D et la complexité d’ASN.1
- Il termine avec humour en rêvant du jour où il pourra écrire sur son CV « compilateur ASN.1 + expérience d’implémentation TLS 1.3 »,
tout en revenant avec lucidité sur la progression du développeur et la réalité du secteur
1 commentaires
Réactions sur Hacker News
En résumé, l’auteur voulait parler d’ASN.1, du langage D et des compilateurs eux-mêmes.
Mais comme il n’a pas trouvé de format cohérent, il a rassemblé ses réflexions dans un article de blog.
Le résultat n’est peut-être pas totalement abouti, mais le sujet se prête mal à un traitement bref, donc on peut lui pardonner.
Mathématiquement,
{0} ∪ ({2} ∩ {4,5,6,7,8}) = {0}, donc au final une seule valeur est autorisée.Personnellement, j’aime beaucoup D, mais en pratique Go et Rust sont bien plus largement utilisés.
Je compatis profondément avec les galères de l’auteur.
J’adore D, même si je l’ai laissé de côté depuis longtemps.
J’ai aussi déjà travaillé sur des parseurs et des implémentations de protocoles, donc ça m’a d’autant plus intéressé.
« OMG ASN.1 », quel sujet réjouissant.
Je me souviens de l’époque où Internet grandissait et où l’IETF faisait évoluer les protocoles.
À l’époque, les entreprises ne s’intéressaient pas à Internet, et l’université ainsi que l’IETF menaient la danse.
Mais quand les entreprises ont compris qu’il y avait de l’argent à gagner, les Protocol Wars ont commencé.
ASN.1 est un produit de cette guerre et un exemple de la collision entre culture d’entreprise et culture académique.
On pourrait comparer l’entreprise à une « culture de la recette » et le monde académique à une « culture de la fonction ».
Cette différence de mentalité a aussi des résonances avec la culture de développement de l’IA aujourd’hui.
Et quand je pense qu’on aurait pu finir avec un système d’adresses du type « CN=wikipedia, OU=org, C=US » au lieu d’Internet, ça fait froid dans le dos.
En réalité, c’étaient surtout l’ITU et l’ISO.
Puis, à la fin des années 1990, il y a eu une autre « guerre des protocoles », et cette fois l’IETF a perdu.
L’ISO visait la perfection et avançait lentement, tandis que l’IETF allait vite avec une logique de « on corrigera plus tard ».
Résultat, des protocoles se sont figés avec leurs défauts.
Et le fait que les implémentations ASN.1 en C des années 1990 aient été médiocres n’a rien arrangé.
Il existe un proverbe turc qui dit : « Ce n’est pas quelque chose qu’un être humain devrait utiliser ! »
J’aimerais en faire la devise d’une philosophie de design.
Et comme dans Game of Thrones : « Celui qui rend le jugement doit lui-même manier l’épée »,
ceux qui écrivent les specs devraient implémenter eux-mêmes les parseurs.
Si l’approbation d’une spec exigeait la soumission conjointe d’un parseur fonctionnel et de tests, la qualité serait bien meilleure.
J’aime vraiment beaucoup le langage D.
Je suis en train d’implémenter mon propre éditeur de texte de style vim en ne dépendant que de Raylib.
Les points forts de D sont les suivants :
version(unittest)permettent de gérer facilement le code réservé aux testsEn consultant la documentation ou en demandant à ChatGPT, j’ai toujours pu trouver une solution élégante.
Sur le plan de la philosophie de conception, il frôle la perfection, mais si ses outils et son écosystème avaient été au niveau de Rust ou Go, il aurait eu bien plus de succès.
La bibliothèque standard Phobos accumulait trop de petites irritations, au point que j’ai fini par abandonner.
Une nouvelle version, Phobos V3, est en préparation, mais comme les effectifs sont réduits, j’oscille entre espoir et inquiétude.
« Est-ce que j’ai déjà dit qu’ASN.1 était complexe ? »
Le schéma comme le format des données sont complexes, mais cette complexité est en grande partie ignorable.
Je n’utilise pas la notation de schéma ASN.1 et j’ai écrit directement une implémentation DER en C.
DER est selon moi le seul encodage standard réellement valable.
J’ai aussi créé mes propres formats d’encodage comme DSER, SDSER et TER.
Des structures comme
ANY DEFINED BYrestent encore très utiles,et j’ai même ajouté une fonctionnalité non standard, OBJECT IDENTIFIER RELATIVE TO, pour obtenir un encodage plus efficace.
J’ai moi aussi déjà écrit un compilateur ASN.1.
Je n’ai implémenté qu’une partie des fonctionnalités de X.681 à X.683, mais j’ai permis de décoder récursivement un certificat complet en un seul appel au codec.
ASN.1 n’est pas juste une syntaxe simple, c’est un système de types puissant.
C’est sous-estimé, mais c’est une technologie vraiment impressionnante.
J’ai autrefois créé un compilateur ASN.1 pour Swift.
C’était le projet ASN1Codable, qui s’appuyait sur libasn1 de Heimdal
pour convertir ASN.1 en AST JSON et ainsi simplifier le parsing.
Le « transformons ça en JSON » sonne au fond comme le cri d’un développeur blessé 😄
Étrangement, travailler sur ASN.1 me paraît agréable.
J’aimerais un jour écrire moi-même un compilateur ASN.1 pour Rust.
Les implémentations Rust actuelles reposent surtout sur des macros derive ou du chaînage manuel, ce que je trouve frustrant.
En général, quand on implémente un standard, on réalise 80 % des fonctionnalités en 20 % du temps,
mais les 20 % restants d’ASN.1 peuvent prendre toute une vie.
J’ai autrefois étendu le parseur ASN.1 de la base de code de Netscape pour prendre en charge PKCS#12.
J’ai regretté d’avoir appris les standards RSA et les définitions ASN.1 un peu trop en profondeur,
mais j’ai un grand respect pour la persévérance et le léger masochisme de l’auteur du blog.