PyDERASN: como escrevi a biblioteca ASN.1 com slots e blobs

ASN.1 é um padrão (ISO, ITU-T, GOST) de uma linguagem que descreve informações estruturadas, bem como regras de codificação para essas informações. Para mim, como programador, esse é apenas outro formato para serializar e apresentar dados, juntamente com JSON, XML, XDR e outros. É extremamente comum em nossa vida cotidiana, e muitas pessoas se deparam com isso: em comunicações móveis, telefônicas, VoIP (UMTS, LTE, WiMAX, SS7, H.323), em protocolos de rede (LDAP, SNMP, Kerberos), em tudo Em relação à criptografia (padrões X.509, CMS, PKCS), em cartões bancários e passaportes biométricos, e muito mais.

Este artigo discute a biblioteca PyDERASN : Python ASN.1 usada ativamente em projetos relacionados à criptografia no Atlas .

Meu

De fato, não vale a pena recomendar o ASN.1 para tarefas criptográficas: o ASN.1 e seus codecs são complexos. Isso significa que o código não será simples, mas é sempre um vetor de ataque extra. Basta olhar para a lista de vulnerabilidades nas bibliotecas ASN.1. Bruce Schneier, em sua engenharia de criptografia, também não recomenda o uso desse padrão devido à sua complexidade: "A codificação TLV mais conhecida é a ASN.1, mas é incrivelmente complexa e evitamos isso". Mas, infelizmente, hoje temos infraestruturas de chave pública que usam ativamente certificados X.509 , CRL, OCSP, TSP, CMP, CMC , mensagens CMS e uma tonelada de padrões PKCS . Portanto, você deve poder trabalhar com o ASN.1 se estiver fazendo algo relacionado à criptografia.

O ASN.1 pode ser codificado de várias maneiras / codecs:

  • BER (Regras Básicas de Codificação)
  • CER (Regras de codificação canônica)
  • DER (Regras de Codificação Distintas)
  • GSER (Regras de codificação de seqüência genérica)
  • JER (regras de codificação JSON)
  • LWER (Regras de codificação leves)
  • OER (Regras de codificação de octetos)
  • PER (regras de codificação compactada)
  • SER (Regras de codificação específicas de sinalização)
  • XER (regras de codificação XML)

e vários outros. Mas nas tarefas criptográficas na prática, duas são usadas: BER e DER. Mesmo em documentos XML assinados ( XMLDSig , XAdES ), ainda haverá objetos ASN.1 DER codificados em Base64, como no protocolo ACME baseado em JSON do Let's Encrypt. Você pode entender melhor todos esses codecs e princípios de codificação BER / CER / DER em artigos e livros: ASN.1 em palavras simples , ASN.1 - Comunicação entre sistemas heterogêneos por Olivier Dubuisson , ASN.1 Concluído pelo Prof John Larmouth .

O BER é um formato TLV orientado a byte binário (por exemplo, PER, popular em comunicações móveis - orientado a bits). Cada elemento é codificado na forma de: uma tag ( T ag) que identifica o tipo de elemento que está sendo codificado (número inteiro, sequência, data, etc.), comprimento (comprimento) do conteúdo e o conteúdo em si (valor). O BER opcionalmente permite que você não especifique um valor de comprimento, definindo um valor especial de comprimento indefinido e terminando com uma mensagem de fim de octetos. Além da codificação de comprimento, o BER possui muita variabilidade no método de codificação de tipos de dados, como

  • INTEGER, IDENTIFICADOR DE OBJETOS, BIT STRING e o comprimento do elemento não podem ser normalizados (não codificados na forma mínima);
  • BOOLEAN é verdadeiro para qualquer conteúdo diferente de zero;
  • BIT STRING pode conter zero bits "extra";
  • BIT STRING, OCTET STRING e todos os seus tipos de string derivados, incluindo data / hora, podem ser divididos em partes (pedaço) de comprimento variável, cujo comprimento durante a (des) codificação não é conhecido antecipadamente;
  • UTCTime / GeneralizedTime pode ter métodos diferentes para definir o deslocamento do fuso horário e as frações zero "extras" de segundos;
  • Os valores da SEQUÊNCIA PADRÃO podem ou não ser codificados;
  • Valores nomeados dos últimos bits em BIT STRING podem ser opcionalmente codificados;
  • SEQUENCE (OF) / SET (OF) pode ter uma ordem arbitrária de elementos.

Para todas as opções acima, nem sempre é possível codificar dados para que sejam idênticos à forma original. Portanto, um subconjunto das regras foi inventado: o DER é uma regulamentação estrita de apenas um método de codificação válido, essencial para tarefas criptográficas, nas quais, por exemplo, alterar um bit invalida a assinatura ou a soma de verificação. O DER tem uma desvantagem significativa: os comprimentos de todos os elementos devem ser conhecidos antecipadamente durante a codificação, o que não permite a serialização de dados por fluxo. O codec CER está livre dessa desvantagem, garantindo da mesma forma uma apresentação inequívoca dos dados. Infelizmente (ou felizmente, não temos decodificadores ainda mais complexos?), Ele não se tornou popular. Portanto, na prática, encontramos um uso "misto" de dados codificados em BER e DER. Como o CER e o DER são um subconjunto do BER, qualquer decodificador BER é capaz de processá-los.

Problemas com pyasn1


No trabalho, escrevemos muitos programas Python relacionados à criptografia. E há alguns anos, praticamente não havia escolha de bibliotecas gratuitas: ou são bibliotecas de nível muito baixo que permitem codificar / decodificar, por exemplo, um número inteiro e um cabeçalho de estrutura, ou essa é a biblioteca pyasn1 . Nós vivemos nele por vários anos e, a princípio, ficamos muito satisfeitos, pois permite trabalhar com estruturas ASN.1 como objetos de alto nível: por exemplo, um objeto de certificado X.509 decodificado permite acessar seus campos através da interface do dicionário: cert ["tbsCertificate"] ["SerialNumber"] nos mostrará o número de série deste certificado. Da mesma forma, você pode "coletar" objetos complexos trabalhando com eles como em listas, dicionários e, em seguida, basta chamar a função pyasn1.codec.der.encoder.encode e obter uma representação serializada do documento.

No entanto, fraquezas, problemas e limitações foram revelados. Infelizmente, ainda existem erros no pyasn1: no momento da escrita, no pyasn1, um dos tipos básicos, GeneralizedTime, é incorretamente decodificado e codificado.

Em nossos projetos, para economizar espaço, geralmente armazenamos apenas o caminho para o arquivo, o deslocamento e o comprimento em bytes do objeto ao qual queremos nos referir. Por exemplo, um arquivo assinado arbitrário provavelmente estará localizado na estrutura do CMS SignedData ASN.1:

0 [1,3,1018] ContentInfo SEQUENCE 4 [1,1, 9] . contentType: ContentType OBJECT IDENTIFIER 1.2.840.113549.1.7.2 (id_signedData) 19-4 [0,0,1003] . content: [0] EXPLICIT [UNIV 16] ANY 19 [1,3, 999] . . DEFINED BY id_signedData: SignedData SEQUENCE 23 [1,1, 1] . . . version: CMSVersion INTEGER v3 (03) 26 [1,1, 19] . . . digestAlgorithms: DigestAlgorithmIdentifiers SET OF [...] 47 [1,3, 769] . . . encapContentInfo: EncapsulatedContentInfo SEQUENCE 51 [1,1, 8] . . . . eContentType: ContentType OBJECT IDENTIFIER 1.3.6.1.5.5.7.12.2 (id_cct_PKIData) 65-4 [1,3, 751] . . . . eContent: [0] EXPLICIT OCTET STRING 751 bytes OPTIONAL      751  820 [1,2, 199] . . . signerInfos: SignerInfos SET OF 823 [1,2, 196] . . . . 0: SignerInfo SEQUENCE 826 [1,1, 1] . . . . . version: CMSVersion INTEGER v3 (03) 829 [0,0, 22] . . . . . sid: SignerIdentifier CHOICE subjectKeyIdentifier [...] 956 [1,1, 64] . . . . . signature: SignatureValue OCTET STRING 64 bytes . . . . . . C1:B3:88:BA:F8:92:1C:E6:3E:41:9B:E0:D3:E9:AF:D8 . . . . . . 47:4A:8A:9D:94:5D:56:6B:F0:C1:20:38:D2:72:22:12 . . . . . . 9F:76:46:F6:51:5F:9A:8D:BF:D7:A6:9B:FD:C5:DA:D2 . . . . . . F3:6B:00:14:A4:9D:D7:B5:E1:A6:86:44:86:A7:E8:C9 

e podemos obter o arquivo assinado original em um deslocamento de 65 bytes, 751 bytes. pyasn1 não armazena essas informações em seus objetos decodificados. O chamado TLVSeeker foi escrito - uma pequena biblioteca que permite decodificar as tags e comprimentos de objetos, na interface da qual comandamos "ir para a próxima tag", "ir dentro da tag" (ir dentro da SEQUÊNCIA do objeto), "ir para a próxima tag", "informar o seu deslocamento e o comprimento do objeto em que estamos. " Foi uma caminhada “manual” nos dados serializados pelo ASN.1 DER. Mas não foi possível trabalhar com dados serializados do BER dessa maneira, porque, por exemplo, a sequência de bytes OCTET STRING pode ser codificada como várias partes.

Outra desvantagem de nossas tarefas pyasn1 é a incapacidade de entender de objetos decodificados se um determinado campo estava presente no SEQUENCE ou não. Por exemplo, se a estrutura contiver o campo SEQUÊNCIA DE SEGUNDO campo OPCIONAL, ele poderá estar completamente ausente nos dados recebidos (OPCIONAL), mas poderá estar presente, mas ao mesmo tempo ter comprimento zero (lista vazia). No caso geral, isso não pôde ser esclarecido. E isso é necessário para uma verificação rigorosa da validade dos dados recebidos. Imagine que alguma autoridade de certificação emitisse um certificado com dados "não totalmente" válidos do ponto de vista dos esquemas ASN.1! Por exemplo, a autoridade de certificação TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı em seu certificado raiz ultrapassava os limites permitidos do RFC 5280 para a duração do componente em questão - não pode ser honestamente decodificado de acordo com o esquema. O codec DER exige que um campo cujo valor seja DEFAULT não seja codificado durante a transmissão - esses documentos são encontrados na vida, e a primeira versão do PyDERASN permitiu conscientemente esse comportamento inválido (do ponto de vista do DER) por uma questão de compatibilidade com versões anteriores.

Outra limitação é a incapacidade de descobrir facilmente de que forma (BER / DER) um ou outro objeto foi codificado na estrutura. Por exemplo, o padrão CMS diz que a mensagem é codificada em BER, mas o campo assinadoAttrs, sobre o qual a assinatura criptográfica é formada, deve estar no DER. Se decodificarmos com o DER, recairemos sobre o processamento do próprio CMS; se decodificarmos com o BER, não saberemos de que forma era assinadoAttrs. Como resultado, será necessário que o TLVSeeker (o análogo não esteja no pyasn1) procure a localização de cada um dos campos do assinadoAttrs e deve ser decodificado separadamente pelo DER a partir da visualização serializada.

A possibilidade de processamento automático dos campos DEFINED BY, que são muito comuns, era muito desejável para nós. Após decodificar a estrutura ASN.1, podemos ter muitos campos restantes, os quais devem ser processados ​​ainda mais de acordo com o esquema selecionado com base no IDENTIFICADOR DE OBJETO especificado no campo de estrutura. No código Python, isso significa escrever um if e, em seguida, chamar o decodificador para o campo ANY.

O advento do PyDERASN


No Atlas, regularmente, tendo encontrado problemas ou modificando os programas gratuitos usados, enviamos patches para o topo. No pyasn1, enviamos aprimoramentos várias vezes, mas o código pyasn1 não é o mais fácil de entender e, algumas vezes, ocorrem alterações incompatíveis na API, que nos atingem nas mãos. Além disso, estamos acostumados a escrever testes com testes generativos, o que não era o caso no pyasn1.

Um belo dia, decidi que tinha que suportar isso e estava na hora de tentar escrever minha própria biblioteca com __slot__s, offset s e blobs belamente exibidos! Apenas criar um codec ASN.1 não seria suficiente - você precisa transferir todos os nossos projetos dependentes para ele, e são centenas de milhares de linhas de código nas quais há muito trabalho com estruturas ASN.1. Esse é um dos requisitos: facilidade de tradução do código pyasn1 atual. Depois de passar minhas férias inteiras, escrevi esta biblioteca e transferi todos os projetos para ela. Como eles têm quase 100% de cobertura por testes, isso também significava que a biblioteca estava totalmente operacional.

Da mesma forma, o PyDERASN tem quase 100% de cobertura de teste. O teste generativo é usado com a maravilhosa biblioteca de hipóteses . Também foram executadas imagens em 32 máquinas nucleares. Apesar do fato de não termos quase nenhum código Python2, o PyDERASN ainda mantém compatibilidade com ele e, por isso, possui uma única dependência de seis . Além disso, ele é testado no conjunto de testes de conformidade ASN.1: 2008 .

O princípio de trabalhar com ele é semelhante ao pyasn1 - trabalhando com objetos Python de alto nível. A descrição dos circuitos ASN.1 é semelhante.

 class TBSCertificate(Sequence): schema = ( ("version", Version(expl=tag_ctxc(0), default="v1")), ("serialNumber", CertificateSerialNumber()), ("signature", AlgorithmIdentifier()), ("issuer", Name()), ("validity", Validity()), ("subject", Name()), ("subjectPublicKeyInfo", SubjectPublicKeyInfo()), ("issuerUniqueID", UniqueIdentifier(impl=tag_ctxp(1), optional=True)), ("subjectUniqueID", UniqueIdentifier(impl=tag_ctxp(2), optional=True)), ("extensions", Extensions(expl=tag_ctxc(3), optional=True)), ) 

No entanto, PyDERASN tem uma aparência de digitação forte. Em pyasn1, se o campo fosse do tipo CMSVersion (INTEGER), ele poderia receber um int ou INTEGER. O PyDERASN exige estritamente que o objeto atribuído seja exatamente CMSVersion. Além de escrever o código Python3, usamos anotações de digitação , para que nossas funções não tenham argumentos incompreensíveis como def func (serial, conteúdo), mas def func (serial: CertificateSerialNumber, contents: EncapsulatedContentInfo) e o PyDERASN ajuda a acompanhar tais código.

Ao mesmo tempo, o PyDERASN possui concessões extremamente convenientes para essa mesma digitação. pyasn1 não permitiu que SubjectKeyIdentifier (). subtype (implicitTag = Tag (...)) atribua um objeto SubjectKeyIdentifier () (sem a IMPLICIT TAG necessária) e geralmente precisava copiar e recriar objetos apenas devido às tags IMPLICIT / EXPLICIT alteradas. O PyDERASN observa estritamente apenas o tipo básico - ele substituirá automaticamente as tags de uma estrutura ASN.1 existente. Isso simplifica bastante o código do aplicativo.

Se ocorrer um erro durante a decodificação, no pyasn1 não será fácil entender exatamente onde ocorreu. Por exemplo, no certificado turco já mencionado, obtemos este erro: UTF8String (tbsCertificate: issuer: rdnSequence: 3: 0: value: DEFINED BY 2.5.4.10:utf8String) (138) limites não satisfeitos: 1 ⇐ 77 ⇐ 64 Ao escrever o ASN .1 as pessoas podem cometer erros e facilita a depuração de aplicativos ou a descoberta de problemas nos documentos codificados do lado oposto.

A primeira versão do PyDERASN não suportava a codificação BER. Apareceu muito mais tarde e o processamento UTCTime / GeneralizedTime com fusos horários ainda não é suportado. Isso virá no futuro, porque o projeto é escrito principalmente em tempo livre.

Também na primeira versão não houve trabalho com os campos DEFINED BY. Poucos meses depois, essa oportunidade apareceu e começou a ser usada ativamente, reduzindo significativamente o código do aplicativo - em uma operação de decodificação, foi possível desmontar toda a estrutura completamente. Para fazer isso, no esquema, quais campos são definidos que "determinam". Por exemplo, uma descrição de um esquema do CMS:

 class ContentInfo(Sequence): schema = ( ("contentType", ContentType(defines=((("content",), { id_authenticatedData: AuthenticatedData(), id_digestedData: DigestedData(), id_encryptedData: EncryptedData(), id_envelopedData: EnvelopedData(), id_signedData: SignedData(), }),))), ("content", Any(expl=tag_ctxc(0))), ) 

diz que se contentType contiver um OID com id_signedData, o campo de conteúdo (localizado na mesma SEQUENCE) precisará ser decodificado usando o esquema SignedData. Por que existem tantos suportes? Um campo pode "definir" vários campos ao mesmo tempo, como é o caso das estruturas EnvelopedData. Os campos definidos são identificados pelo chamado caminho de decodificação - ele define a localização exata de qualquer elemento em todas as estruturas.

Nem sempre é desejável ou nem sempre é possível introduzir imediatamente essas definições no circuito. Pode haver casos específicos de aplicativos em que OIDs e estruturas são conhecidas apenas em um projeto de terceiros. O PyDERASN fornece a capacidade de especificar essas definições no momento da decodificação da estrutura:

 ContentInfo().decode(data, ctx={"defines_by_path": (( ( "content", DecodePathDefBy(id_signedData), "certificates", any, "certificate", "tbsCertificate", "extensions", any, "extnID", ), ((("extnValue",), { id_ce_authorityKeyIdentifier: AuthorityKeyIdentifier(), id_ce_basicConstraints: BasicConstraints(), [...] id_ru_subjectSignTool: SubjectSignTool(), }),), ),)}) 

Aqui dizemos que, no CMS SignedData, para todos os certificados anexados, decodifique todas as suas extensões (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool, etc.). Indicamos através do caminho de decodificação qual elemento "substituir" define, como se fosse definido no circuito.

Finalmente, o PyDERASN tem a capacidade de trabalhar a partir da linha de comando para decodificar arquivos ASN.1 e possui uma rica e bonita impressão . Você pode decodificar um ASN.1 arbitrário ou especificar um esquema claramente definido e ver algo assim:

Exemplo bonito de impressão

Informações exibidas: deslocamento do objeto, comprimento do tag, comprimento do comprimento, comprimento do conteúdo, presença de EOC (fim de octetos), sinalizador de codificação BER, sinalizador de codificação de comprimento indefinido, comprimento e deslocamento da marca EXPLICIT (se houver), profundidade de aninhamento de objeto em estruturas, valor da tag IMPLICIT / EXPLICIT, nome do objeto de acordo com o esquema, seu tipo ASN.1 básico, número de série dentro de SEQUENCE / SET OF, valor CHOICE (se houver), nome legível por humanos INTEGER / ENUMERATED / BIT STRING de acordo com o esquema, valor de qualquer tipo básico , Flag PADRÃO / OPCIONAL do circuito, um sinal de que o objeto foi decodificado automaticamente como DEFINIDO POR e depois gm de OID e isso aconteceu, chelovekochitaemy OID.

O bonito sistema de impressão é feito especialmente para gerar uma sequência de objetos PP que já são visualizados por meios separados. A captura de tela mostra o renderizador em texto colorido simples. Existem renderizadores no formato JSON / HTML para que isso possa ser visto com destaque no navegador ASN.1 como no projeto asn1js .

Outras bibliotecas


Este não era o objetivo, mas o PyDERASN era significativamente mais rápido que o pyasn1. Por exemplo, a decodificação de arquivos CRL de tamanhos de megabytes pode levar tanto tempo que você precisa pensar em formatos intermediários para armazenar dados (rápido) e alterar a arquitetura dos aplicativos. pyasn1 decodifica CRL CACert.org no meu laptop por mais de 20 minutos, enquanto PyDERASN em apenas 28 segundos! Existe um projeto asn1crypto destinado a trabalhar rapidamente com estruturas criptográficas: ele decodifica (completamente, não preguiçosamente) a mesma CRL em 29 segundos, mas consome quase o dobro de RAM ao executar no Python3 (983 MiB versus 498), e 3,5 vezes no Python2 (1677 contra 488), enquanto pyasn1 consome 4,3 vezes mais (2093 contra 488).

o asn1crypto, que mencionei, não consideramos, porque o projeto estava em sua infância e ainda não tínhamos ouvido falar. Agora eles também não começariam a olhar na direção dele, pois eu imediatamente descobri que o mesmo GeneralizedTime não é arbitrário e, quando serializado, ele remove silenciosamente frações de segundo. Isso é aceitável para trabalhar com certificados X.509, mas em geral não funcionará.

No momento, o PyDERASN é o mais rigoroso dos decodificadores gratuitos Python / Go DER que eu conheço. Na biblioteca encoding / asn1 do meu Go favorito, não há uma verificação rigorosa nas seqüências de caracteres OBJECT IDENTIFIER e UTCTime / GeneralizedTime. Às vezes, o rigor pode interferir (principalmente devido à compatibilidade com aplicativos antigos que ninguém corrigirá), portanto, no PyDERASN durante a decodificação, você pode passar por várias verificações de enfraquecimento das configurações .

O código do projeto tenta ser o mais simples possível. A biblioteca inteira é um único arquivo. O código é escrito com ênfase na facilidade de entendimento, sem desempenho desnecessário e otimizações de código DRY. Como eu já disse, ele não suporta a decodificação BER completa de seqüências UTCTime / GeneralizedTime, bem como tipos de dados REAL, OID RELATIVO, EXTERNO, INSTANCE DE, PDV EMBUTIDO, CHARACTER STRING. Em todos os outros casos, pessoalmente, não vejo razão para usar outras bibliotecas no Python.

Como todos os meus projetos, como PyGOST , GoGOST , NNCP , GoVPN , PyDERASN é um software totalmente gratuito, distribuído sob os termos do LGPLv3 + , e está disponível para download gratuito. Exemplos de uso estão aqui nos testes do PyGOST .

Sergey Matveev , banco de cifras , membro da Open Society Foundation Foundation , desenvolvedor Python / Go, especialista principal do FSUE “STC Atlas” .

Source: https://habr.com/ru/post/pt444272/


All Articles