- Um desenvolvedor compartilha a jornada técnica e mental de implementar por conta própria um compilador ASN.1 em D (dasn1)
- O projeto tem como objetivo a implementação de x.509 e TLS 1.3, com suporte ao complexo processamento de codificação DER do ASN.1
- O texto aborda em detalhes a dificuldade estrutural do ASN.1, a complexidade de implementar as especificações x.680~x.683 e formas de usar metaprogramação em D
- Explica concretamente como recursos de D como
static import, templates mixin, typeof() e alias this foram úteis na geração de código e no design de AST/IR
- O autor descreve o ASN.1 como “doloroso, mas muito enriquecedor”, relatando com franqueza as dificuldades reais e as recompensas de criar um compilador
Visão geral e motivação do projeto
- O autor está desenvolvendo um framework de I/O assíncrono em D chamado Juptune, e precisou lidar diretamente com a codificação ASN.1 DER para implementar TLS
- Para fazer o parsing da estrutura de certificados x.509 no TLS, foi necessário entender a forma complexa como o ASN.1 representa dados
- O projeto começou como um desafio pessoal voltado a aprendizado e diversão, e já avançou até o ponto de conseguir fazer o parsing de alguns certificados com sucesso
- Embora o ASN.1 seja um padrão antigo, da década de 1990, ele ainda é usado amplamente em sistemas modernos como TLS, SNMP e LDAP
- O autor comenta que “ASN.1 é amplamente usado no mundo, mas a maioria dos desenvolvedores nem sabe que ele existe”
O que é ASN.1
- ASN.1 (Abstract Syntax Notation One) é uma linguagem para definir e codificar estruturas de dados, uma espécie de “ancestral do Protobuf”
- O padrão é composto pela notação (x.680~x.683) e pelas regras de codificação (BER, CER, DER, PER, XER, JER etc.)
- BER: formato TLV básico, com suporte a comprimento indefinido
- CER: variante restrita de BER, usando sempre comprimento indefinido
- DER: subconjunto determinístico de BER, usado como padrão em criptografia
- PER/OER: codificação compactada em nível de bits
- XER/JER: codificação baseada em XML e JSON
- A grande variedade de codificações torna tudo complexo, mas também oferece alta flexibilidade e extensibilidade
A complexidade da notação ASN.1
- O padrão-base do ASN.1 é o x.680, e as especificações de extensão (x.681~x.683) são escritas em um estilo acadêmico extremamente difícil
- Mesmo só com x.680 já é possível implementar bastante coisa, mas a presença de muitas regras de transformação semântica e variações sintáticas eleva muito a dificuldade
- O x.681 define o sistema de Information Object Class e oferece uma sintaxe própria de inicialização
- Ex.:
CALLED &name [WHO IS &age YEARS OLD]
- O x.682 define Table Constraint, e o x.683 define tipos parametrizados (Parameterized)
- É um conceito parecido com genéricos em D, aceitando tanto tipos quanto valores como parâmetros
Recursos interessantes do ASN.1
- Sistema de restrições (Constraint): permite declarar diretamente o intervalo de valores ou o tamanho na definição do tipo
- Ex.:
UInt8 ::= INTEGER (0..255)
- Suporte a operadores
SIZE, UNION(|) e INTERSECTION(^)
- Sistema de versionamento: usa
OBJECT IDENTIFIER para distinguir claramente versões de módulos
- Ex.:
id-pkix1-implicit(19) vs id-mod-pkix1-implicit-02(59)
- Permite identificar módulos de forma clara sem colisão de nomes
Por que D favorece a geração de código
- O
static import de D evita conflitos de nomes e permite preservar exatamente os nomes de tipos ASN.1
- O recurso de busca local ao módulo (
.Type1) limita com clareza a resolução de símbolos
- Com
typeof(), o tipo pode ser inferido automaticamente, dispensando gestão manual na geração de código
- O suporte a vírgula final (
trailing comma) simplifica a geração de código
- Graças à concatenação de constantes em tempo de compilação, é possível montar strings até mesmo em funções
@nogc
Exemplos de implementação com recursos da linguagem D
Nós de AST baseados em template mixin
- O recurso de
mixin template de D foi usado para definir nós da árvore sintática abstrata (AST) do ASN.1
- Cada tipo de nó (
List, Container, OneOf) é reutilizado como template
- Em vez de uma herança complexa, a implementação foi simplificada com cópia de código em tempo de compilação
API baseada em templates e validação em tempo de compilação
- O nó
Container inclui vários nós filhos e realiza validação de tipos em tempo de compilação
- Isso permite acesso seguro no formato
node.getNode!Asn1TagDefaultNode
- O nó
OneOf armazena um entre vários tipos e oferece pattern matching por meio da função match
- Como todos os handlers de tipo precisam ser definidos, isso garante segurança em tempo de compilação
Uso do pacote experimental de gerenciamento de memória de D
- Com
std.experimental.allocator, foi possível implementar criação e liberação de objetos em ambiente @nogc
- Um alocador customizado foi montado combinando elementos como
Region e StatsCollector
- Ainda assim, o pacote continua em estado experimental há 10 anos
Recurso alias this
- Com
alias this, o autor fez com que structs wrapper se comportassem como seus campos internos
- Ex.: é possível fazer casts concisos como
cast(Asn1ValueReferenceIr)item
version(unittest)
- A palavra-chave
version(unittest) foi usada para definir funções exclusivas para testes, que não entram no build real
Harness de testes com templates + with()
- A lógica comum de testes foi transformada em template, e a instrução
with() permitiu escrever testes de forma mais concisa
- Em vez de
Harness.T(), é possível chamar simplesmente T()
Principais dificuldades enfrentadas na implementação
Sintaxe de sequência de valores (Value Sequence Syntax)
- As várias formas de sintaxe de valor que começam com
{} são ambíguas dependendo do contexto
- A complexidade é tanta que um comentário no parser chega a dizer “isso não é divertido”
- Como a análise sintática e a análise semântica foram separadas, a dificuldade de tratamento aumentou
Falta de clareza na especificação
- Existem comportamentos não explicitamente descritos na documentação, como regras que exigem tratar certas tags como
EXPLICIT em condições específicas
- O próprio método de versionamento de módulos também não é definido com clareza
Necessidade de implementar restrições em triplo
- Para validação sintática
- Para validação de valores
- Para geração de código em tempo de execução
- Ao lidar com
UNION e INTERSECTION, até mesmo montar mensagens de erro fica complexo
A ilusão de nós IR imutáveis
- O autor imaginava que, depois de converter a AST em IR, não seria mais necessário modificar nada,
mas transformações semânticas como AUTOMATIC TAGS exigiram alterações nos dados
A complexidade total do ASN.1
- O x.509 usa apenas uma sintaxe antiga e por isso é relativamente simples, mas as especificações mais novas exigem implementar x.681~x.683
- Por isso, o ASN.1 quase não é usado fora de contextos acadêmicos ou comerciais específicos
O problema de ANY DEFINED BY
ANY DEFINED BY é uma estrutura cujo tipo muda conforme o valor de outro campo
- O
dasn1 não implementa isso e usa em seu lugar um intrinsic customizado Dasn1-Any
- Na decodificação real, isso precisa ser tratado manualmente
Sobrecarga de informação
- Ao tocar simultaneamente em ASN.1, x.68x, x.690, Juptune e outros projetos, tornou-se difícil manter o contexto do codebase
A realidade de fazer um compilador
- O trabalho envolve milhares de visitantes de nós, código repetitivo e implementações com diferenças mínimas, sendo por isso cansativo e árduo
- Ainda assim, cada etapa trouxe grande sensação de realização e aprendizado
- O autor relembra que “ninguém vai usar isso, mas eu ganhei uma experiência real de compilador”
- No fim, fecha o texto com a piada: “não mexa com ASN.1, isso muda a sua vida”
Conclusão
- Mesmo após um ano de trabalho, o
dasn1 ainda está incompleto,
mas a experiência serviu para compreender profundamente o potencial de D e a complexidade do ASN.1
- O texto termina com humor, sonhando com o dia em que o autor poderá colocar no currículo a experiência de “compilador ASN.1 + implementação de TLS 1.3”,
revisitando ao mesmo tempo o crescimento do desenvolvedor e a realidade da indústria
1 comentários
Comentários do Hacker News
Resumindo, eu queria falar sobre ASN.1, a linguagem D e compiladores em si
Mas não consegui encontrar um formato consistente, então juntei essas ideias em um post de blog
O nível de acabamento não é alto, mas peço compreensão, porque é um tema difícil de tratar brevemente
intersection example) não está funcionando como pretendidoMatematicamente,
{0} ∪ ({2} ∩ {4,5,6,7,8}) = {0}, então no fim só um único valor é permitidoPessoalmente eu gosto muito de D, mas, na prática, Go e Rust são muito mais usados
Tenho muita empatia pelo sofrimento do autor
Adoro D, mas faz muito tempo que não mexo com ela
Já tive experiência com parsers e implementação de protocolos, então achei ainda mais interessante
“OMG ASN.1”, que tema bom de rever
Lembro da época em que a internet estava crescendo e a IETF fazia os protocolos evoluírem
Naquele tempo, as empresas não tinham interesse na internet, e a academia e a IETF puxavam isso
Mas quando as empresas perceberam que dava dinheiro, começaram as Protocol Wars
ASN.1 é um produto dessa guerra e um exemplo do choque entre cultura corporativa e cultura acadêmica
Dá para comparar empresas a uma “cultura de receita” e a academia a uma “cultura de funcionalidade”
Essa diferença de mentalidade também traz reflexões para a cultura de desenvolvimento de IA hoje
Pensar que poderíamos ter seguido por um sistema de endereços como “CN=wikipedia, OU=org, C=US”, em vez da internet, dá arrepios
Na prática, ITU e ISO estavam no centro disso
Depois, no fim dos anos 90, houve outra “guerra de protocolos”, e dessa vez a IETF perdeu
en-shittification) da internetA ISO buscava a perfeição e ficou lenta, enquanto a IETF se movia rápido com a atitude de “a gente corrige depois”
Como resultado, enfrentaram o problema de protocolos que acabavam se cristalizando
Outro problema foi que as implementações de ASN.1 em C nos anos 1990 eram péssimas
Há um ditado turco que diz: “Isto não é uma coisa para seres humanos usarem!”
Eu gostaria de adotar isso como lema de filosofia de design
E, assim como a fala de Game of Thrones de que “quem profere a sentença deve empunhar a espada”,
quem cria a especificação deveria implementar o parser pessoalmente
Se as coisas mudassem para que uma especificação só fosse aprovada com um parser funcional e testes junto, a qualidade provavelmente melhoraria muito
Eu gosto muito da linguagem D
Estou implementando por conta própria um editor de texto estilo vim dependendo apenas de Raylib
As vantagens de D são as seguintes
version(unittest)facilita muito gerenciar código exclusivo de testeConsultando a documentação ou perguntando ao ChatGPT, eu sempre conseguia encontrar uma solução elegante
Em termos de filosofia de design ela chega perto da perfeição, mas, se as ferramentas e o ecossistema estivessem no nível de Rust ou Go, teria feito muito mais sucesso
noisy) com o tempoA biblioteca padrão Phobos tem tantos pequenos incômodos que eu acabei desistindo dela
A nova versão, Phobos V3, está em andamento, mas como há pouca gente envolvida, fico meio esperançoso e meio preocupado
“Eu já disse alguma vez que ASN.1 é complexa?”
Tanto a notação de schema quanto o formato de dados são complexos, mas grande parte disso é uma complexidade que dá para ignorar
Eu não uso a notação de schema ASN.1 e escrevi uma implementação de DER em C por conta própria
Acho que DER é a única codificação padrão que realmente presta
Também criei formatos de codificação próprios como DSER, SDSER e TER
Estruturas como
ANY DEFINED BYainda continuam sendo úteis para mim,e, para uma codificação eficiente, também adicionei um recurso não padronizado chamado OBJECT IDENTIFIER RELATIVE TO
Eu também já fiz um compilador ASN.1
Implementei apenas parte dos recursos de X.681~X.683, mas fiz com que fosse possível decodificar um certificado inteiro recursivamente com uma única chamada de codec
ASN.1 não é só uma gramática simples, e sim um sistema de tipos poderoso
É subestimada, mas é uma tecnologia realmente muito boa
No passado eu fiz um compilador ASN.1 para Swift
No projeto ASN1Codable, usando o libasn1 do Heimdal,
converti ASN.1 para uma AST JSON para simplificar o parsing
Falar “vamos converter para JSON” soa, no fundo, como o grito de um desenvolvedor ferido 😄
Estranhamente, trabalhar com ASN.1 parece divertido
Um dia eu gostaria de fazer meu próprio compilador ASN.1 para Rust
As implementações atuais em Rust em geral dependem de macros derive ou de encadeamento manual, o que é uma pena
Em geral, ao implementar um padrão, você consegue concluir 80% dos recursos em 20% do tempo,
mas os 20% restantes de ASN.1 podem levar uma vida inteira
No passado, eu estendi o parser ASN.1 do código-base do Netscape para suportar PKCS#12
Acabei conhecendo os padrões da RSA e as definições de ASN.1 profundamente demais para o meu gosto,
mas deixo meu respeito à persistência e a um leve masoquismo do autor do blog