PyDERASN: como adicionei o suporte a big data

Continuo o último artigo sobre PyDERASN - o codec ASN.1 DER / CER / BER gratuito em Python. Durante o ano passado, desde o momento em que foi escrito, além de todas as pequenas coisas, pequenas correções e verificação de dados ainda mais rigorosa (embora antes já fosse o mais rigoroso dos codecs gratuitos conhecidos por mim), uma funcionalidade para trabalhar com grandes volumes de dados apareceu nesta biblioteca - não rastejando para a RAM. Eu quero falar sobre isso neste artigo.

Navegador ASN.1

Problemas / Tarefas


  • CRL:
    , (CA) X.509 . , CRL (certificate revocation list). CRL CACert.org, 8.72 MiB, PyDERASN- Python 3.6 ( asn1crypto pyasn1 ). 416 . . , . .
  • CMS:
    CMS (Cryptographic Message Syntax)
    // . ,
    SignedData , ,
    , - EnvelopedData
    .

    CMS. , CMS detached data, - , . 10 GiB 20 GiB : 10 GiB . .


Quais objetos ASN.1 na prática podem consumir muitos recursos, ocupam muito espaço na memória? Somente SEQUENCE OF / SET OF contendo vários objetos (centenas de milhares no caso do CACert.org) e todos os tipos de * STRINGs (como no segundo caso problemático). Como uma das virtudes de um programador é a preguiça (de acordo com Larry Wall), tentaremos superar os problemas do consumo de recursos com uma alteração mínima no código da biblioteca.

* Os objetos STRING , do ponto de vista do codec, quase não diferem um do outro e são implementados na biblioteca por uma classe comum. O que é codificação no OCTET STRING no DER?

return tag + len_encode(len(value)) + value

Obviamente, o valor não precisa estar na RAM esse tempo todo. É necessário que seja um objeto semelhante a bytes a partir do qual você pode descobrir o comprimento.

O memoryview satisfaz essas condições. No entanto, o memoryview pode ser feito pelo mmap em qualquer arquivo temporário que já esteja reduzido na memória. Os requisitos de 20 GiB 10 GiB para o banco de dados CMS são copiados pela metade: basta especificar o valor de OCTET STRING s (ou qualquer outro * STRING s) semelhante ao memoryview . Para criá-lo, o PyDERASN possui uma função auxiliar:

from pyderasn import file_mmaped
with open("dump.sql.zst", "rb") as fd:
    ... = OctetString(file_mmaped(fd))

Se quisermos decodificar uma quantidade enorme de dados, o memoryview também poderá ser usado para decodificar:

from pyderasn import file_mmaped
with open("dump.sql.zst", "rb") as fd:
    obj = Schema.decode(file_mmaped(fd))

Consideramos que o problema com * STRING está parcialmente resolvido. Voltar para criar uma CRL enorme. Como é a sua estrutura?

CertificateList SEQUENCE
. tbsCertList: TBSCertList SEQUENCE
. . version: Version INTEGER v2 (01) OPTIONAL
. . signature: AlgorithmIdentifier SEQUENCE
. . issuer: Name CHOICE rdnSequence
. . thisUpdate: Time CHOICE utcTime
. . nextUpdate: Time CHOICE utcTime OPTIONAL
. . revokedCertificates: RevokedCertificates SEQUENCE OF OPTIONAL
. . . 0: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 17 (11)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2003-04-01T14:25:08
. . . 1: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 20 (14)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2002-10-01T02:18:01

                                 [...]

. . . 415753: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341859 (14:79:A3)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T06:51:56
. . . 415754: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341860 (14:79:A4)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T06:53:01
. . . 415755: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341861 (14:79:A5)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T07:25:06
. signatureAlgorithm: AlgorithmIdentifier SEQUENCE
. signatureValue: BIT STRING 4096 bits

Uma longa lista de pequenas estruturas RevokedCertificate . Nesse caso, contendo apenas o número de série do certificado e a hora de sua revogação. Qual é a sua codificação DER?

value = b"".join(revCert.encode() for revCert in revCerts)
return tag + len_encode(len(value)) + value

Apenas concatenação de representações DER de cada elemento individual desta lista. Precisamos ter antecipadamente todas essas centenas de milhares de objetos no decorrer de apenas seriados em 15 bytes de representação do DER? Obviamente não. Portanto, substituímos a lista de objetos por um iterador / gerador. Provavelmente, a criação da CRL será baseada nos dados do DBMS:

def revCertsGenerator(...):
    for row in db.cursor:
        yield RevokedCertificate((
            ("userCertificate", CertificateSerialNumber(row.serial)),
            ("revocationDate", Time(("utcTime", UTCTime(row.revdate)))),
        ))

crl["tbsCertList"]["revokedCertificates"] = revCertsGenerator()

Agora, quase não consumimos memória ao criar uma CRL - precisamos apenas de um local para trabalhar com sua representação DER: nesse caso, estamos falando de algumas dezenas de megabytes (em oposição a uma lista de meio gigabyte de objetos RevokedCertificate). Mas há uma diferença no comportamento: a verificação do tamanho SEQUENCE OF / SET OF ocorre somente depois que o iterador se esgota, e não no momento de atribuir um valor.

Codificação de streaming DER


É impossível. Afinal, esse é o DER - os comprimentos de todos os elementos TLV (tag + length + value) devem ser conhecidos antecipadamente!

Mas eu gostaria muito! Afinal, ainda precisamos de 10 GiB de memória para armazenar a representação DER da cópia do banco de dados: raw = cms.encode () ! Idealmente, quero transmitir a um certo escritor onde a representação serializada seria escrita. Talvez transfira o descritor de arquivo, deixe espaços reservados no arquivo no lugar dos comprimentos e preencha-os fazendo a busca? Infelizmente, o comprimento do comprimento (respectivamente e do espaço reservado) também não é conhecido antecipadamente.

O PyDERASN recebeu a possibilidade de codificação DER de duas passagens. Na primeira passagem, o conhecimento sobre os comprimentos dos objetos é coletado, criando um estado temporário. O segundo é streamingCodificação DER em um determinado escritor , já possível devido ao conhecimento dos comprimentos. A implementação disso foi simples, basta adicionar alguns métodos de duas passagens a cada um dos tipos de base ASN.1. Como a travessia de objetos é estritamente determinada (D - distinta!), Para armazenar os comprimentos necessários, é mantida uma lista simples, na qual, à medida que toda a árvore de objetos é atravessada, são adicionados valores de comprimentos. Para alguns tipos, o comprimento é fixo. Para alguns, é necessário apenas para relatar a um contêiner upstream ( SEQUENCE / SET , SEQUENCE OF / SET OF , EXPLICIT TAG ). Por exemplo, o estado do comprimento de uma lista de dois certificados revogados é semelhante a este:

revCert = RevokedCertificate((
    ("userCertificate", CertificateSerialNumber(123)),
    ("revocationDate", Time(("utcTime", UTCTime(datetime.utcnow())))),
))
revs = RevokedCertificates([revCert, revCert])

(42, [40, 18, 18])

Nesse caso, precisamos saber o tamanho do valor apenas para cada um dos RevokedCertificate e a lista inteira como um todo. A duração dos números inteiros e o tempo codificado (no DER é um comprimento fixo) no estado não é armazenado como desnecessário. Como resultado, para nossa CRL do CACert.org, essa lista leva um pouco mais de 3,5 MiB, e para o CMS gigante, no qual quase todo o peso cai em um único campo com uma cópia do banco de dados, leva cerca de 0,5 KiB.

A codificação de duas passagens é realizada por duas chamadas:

fulllen, state = obj.encode1st()
with open("result", "wb") as fd:
    obj.encode2nd(fd.write, iter(state))

A primeira passagem também relata o comprimento total dos dados, que podem ser usados ​​para verificar o espaço livre ou alocá-lo por alguma chamada posix_fallocate .

Usando a função auxiliar encode2pass (obj), você pode executar a codificação de duas passagens na memória. Pelo que? Pode ser significativamente mais econômico no consumo de memória, já que, no caso da CACert.org CRL, não armazena 416k + pequenas linhas binárias unidas por uma chamada b "". Join () . No entanto, isso requer mais tempo do processador, porque todos os objetos terão que ser percorridos duas vezes.

Agora podemos codificar um CMS arbitrariamente grande no DER, praticamente sem consumir memória. Mas, no caso da CRL, usamos o gerador de revogação de certificado, que será executado após o final da primeira passagem. O que fazer? Apenas reinicialize novamente!

_, state = crl.encode1st()
crl["tbsCertList"]["revokedCertificates"] = revCertsGenerator()
crl.encode2nd(writer, iter(state))

Obviamente, somos obrigados a garantir que o resultado do iterador seja exatamente o mesmo, caso contrário, obteremos um DER quebrado. Se os dados forem obtidos do cursor DBMS, não se esqueça do nível e classificação de isolamento da transação REPEATABLE READ .

Codificação de fluxo real: CER


Como você sabe, o DER (regras de codificação distintas) é um subconjunto do BER (regras básicas de codificação) que regula estritamente as regras de codificação de uma e apenas uma maneira. Isso permite que ele seja usado em tarefas criptográficas. Mas há outro subconjunto digno de nota do BER: CER (regras de codificação canônica). Como o DER, ele possui apenas uma representação possível dos dados. O CER difere do DER em vários detalhes, mas permite executar uma codificação de dados verdadeiramente em fluxo contínuo. Infelizmente, o CER não se tornou tão popular quanto o DER.

Omitindo diferenças não tão perceptíveis (como classificar tags no SET), o CER possui duas diferenças fundamentais em relação ao DER:

  • (constructed, ) indefinite (LENINDEF PyDERASN). , SEQUENCE/SET, SEQUENCE OF/SET OF, EXPLICIT TAG- :

    TAG_CONSTRUCTED || LEN(VALUE) || VALUE
    

    LENINDEF (0x80) EOC ( , ) :

    TAG_CONSTRUCTED || 80 || VALUE || 00 00
    

  • *STRING-, 1000-, chunk- 1000-. , , DER. , 512 DER:

    TAG_PRIMITIVE || LEN(512) || 512B
    

    2048 :

    TAG_CONSTRUCTED || 80 ||
        TAG_PRIMITIVE || LEN(1000) || 1000B ||
        TAG_PRIMITIVE || LEN(1000) || 1000B ||
        TAG_PRIMITIVE || LEN(48) || 48B || 00 00
    

Tudo isso (mais algumas coisinhas) permite que o streaming (com 1000 bytes de buffer para strings) codifique qualquer objeto. Da mesma forma, você pode usar mmap e iteradores. A codificação CER é feita simplesmente chamando o método .encode_cer (writer) . Infelizmente, o PyDERASN ainda não é capaz de verificar a validade do CER durante a decodificação, portanto somos forçados a decodificar os dados como BER.

O padrão CMS, a propósito, requer codificação no BER (tanto o DER quanto o CER, automáticos, são BER). Portanto, podemos codificar nossa cópia enorme do banco de dados no CER CMS sem um DER de duas passagens. No entanto , é necessário que SignedData tenha um elemento SignedAttributes codificado no DER, assim como o Certificado X.509certificados. PyDERASN permite forçar o uso de DER em determinadas estruturas, simplesmente adicionando o atributo der_forced = True .

Decodificação de fluxo: modo evgen


Aprendemos a codificar, mas apenas o mmap ajudará na decodificação . Um decodificador de fluxo “real”, que possui botões de controle na forma de “me dê mais dados”, “aqui está você”, algum tipo de estado - exigiria uma alteração radical do PyDERASN. E, pessoalmente, não acho que seria mais conveniente do que a solução atual.

E a solução atual é extremamente simples. No processo de decodificação, temos em nossas mãos vários objetos primitivos decodificados em nossas mãos, a partir deles são montados componentes superiores (construídos), dos quais outros componentes, etc. ... Acumulamos objetos para torná-los compostos e, chegando ao topo. nos dê um grande objeto. Por que não "distribuir" imediatamente todos os tipos de decodificadosobjetos, assim que aparecem em nossas mãos? Ou seja, retornar não um objeto final, mas um gerador que gera muitos objetos decodificados. De fato, no PyDERASN agora todos os métodos de decodificação se tornaram geradores desses "eventos" (geração de eventos, evgen).

Se habilitarmos o modo de decodificação evgen de nossa enorme CRL do CACert.org, veremos a seguinte figura:

$ python -m pyderasn --schema tests.test_crl:CertificateList --evgen revoke.crl
[][T,L,  V len]
     10   [1,1,      1]   . . version: Version INTEGER v2 (01) OPTIONAL
     15   [1,1,      9]   . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.13
     26   [0,0,      2]   . . . parameters: [UNIV 5] ANY OPTIONAL
     13   [1,1,     13]   . . signature: AlgorithmIdentifier SEQUENCE
     34   [1,1,      3]   . . . . . . type: AttributeType OBJECT IDENTIFIER 2.5.4.10
     39   [0,0,      9]   . . . . . . value: [UNIV 19] AttributeValue ANY
     32   [1,1,     14]   . . . . . 0: AttributeTypeAndValue SEQUENCE
     30   [1,1,     16]   . . . . 0: RelativeDistinguishedName SET OF

                                 [...]

    188   [1,1,      1]   . . . . userCertificate: CertificateSerialNumber INTEGER 17 (11)
    191   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2003-04-01T14:25:08
    191   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
    191   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2003-04-01T14:25:08
    186   [1,1,     18]   . . . 0: RevokedCertificate SEQUENCE
    208   [1,1,      1]   . . . . userCertificate: CertificateSerialNumber INTEGER 20 (14)
    211   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2002-10-01T02:18:01
    211   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
    206   [1,1,     18]   . . . 1: RevokedCertificate SEQUENCE

                                 [...]

9144992   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
9144985   [1,1,     20]   . . . 415755: RevokedCertificate SEQUENCE
    181   [1,4,9144821]   . . revokedCertificates: RevokedCertificates SEQUENCE OF OPTIONAL
      5   [1,4,9144997]   . tbsCertList: TBSCertList SEQUENCE
9145009   [1,1,      9]   . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.13
9145020   [0,0,      2]   . . parameters: [UNIV 5] ANY OPTIONAL
9145007   [1,1,     13]   . signatureAlgorithm: AlgorithmIdentifier SEQUENCE
9145022   [1,3,    513]   . signatureValue: BIT STRING 4096 bits
      0   [1,4,9145534]  CertificateList SEQUENCE

  • No início da decodificação, vimos a tag CertificateList SEQUENCE , o comprimento dos dados, mas ainda não se sabe se o objeto pode ser decodificado até o final. Até agora, estamos apenas no processo de trabalhar nisso.
  • SEQUENCE: version, INTEGER. . , . ( 10)
  • signature, SEQUENCE- : algorithm parameters. OBJECT IDENTIFIER ANY . ( 15, 26)
  • , , signature SEQUENCE , , : . ( 13)
  • , RevokedCertificate . ( 186, 206, ..)
  • tbsCertList . ( 5)
  • CertificateList, SEQUENCE, , , . ( 0)

Obviamente, todas as * STRING e listas ( * OF ) não têm significado real. No caso do DER, conhecendo .offset e .vlen, você pode ler o valor de uma linha de um arquivo (um pedaço de memória?). Os objetos de sequência podem ser coletados e agregados conforme necessário, ao receber todos os eventos.

Caminho decodificado e evgen_mode_upto


Como entender que tipo de objeto, que tipo de INTEGER temos em mãos? Cada objeto possui seu próprio caminho de decodificação, que identifica exclusivamente um objeto específico na estrutura. Por exemplo, para eventos do caminho de decodificação da CACert.org CRL:

tbsCertList:version
tbsCertList:signature:algorithm
tbsCertList:signature:parameters
tbsCertList:signature
tbsCertList:issuer:rdnSequence:0:0:type
                                 [...]
tbsCertList:issuer:rdnSequence
tbsCertList:issuer
                                 [...]
tbsCertList:revokedCertificates:0:userCertificate
tbsCertList:revokedCertificates:0:revocationDate:utcTime
tbsCertList:revokedCertificates:0:revocationDate
tbsCertList:revokedCertificates:0
tbsCertList:revokedCertificates:1:userCertificate
tbsCertList:revokedCertificates:1:revocationDate:utcTime
tbsCertList:revokedCertificates:1:revocationDate
tbsCertList:revokedCertificates:1
                                 [...]
tbsCertList:revokedCertificates:415755:userCertificate
tbsCertList:revokedCertificates:415755:revocationDate:utcTime
tbsCertList:revokedCertificates:415755:revocationDate
tbsCertList:revokedCertificates:415755
tbsCertList:revokedCertificates
tbsCertList
signatureAlgorithm:algorithm
signatureAlgorithm:parameters
signatureAlgorithm
signatureValue

É assim que podemos imprimir a lista de números de série da revogação de certificado a partir desta CRL:

raw = file_mmaped(open("....crl", "rb"))
for decode_path, obj, tail in CertificateList().decode_evgen(raw):
    if (len(decode_path) == 5) and (decode_path[-1] == "userCertificate"):
        print(int(obj))

A estrutura RevokedCertificate pode conter muitas informações, incluindo várias extensões. Tecnicamente, obtemos todos os dados sobre o certificado revogado no modo evgen, mas a agregação de eventos relacionados a um elemento revokedCertificates não é muito conveniente. Como cada Certificado Revoked final , na prática, não ocupa muito espaço, seria ótimo ter tudo a mesma coisa, nem todos os objetos são tão "ordenados" em eventos. PyDERASN permite que a lista especifique caminhos de decodificação nos quais o modo evgen está desativado. Portanto, podemos definir o caminho da decodificação (qualquer elemento da lista ("tbsCertList", "revokedCertificates") ) no qual queremos obter o objeto RevokedCertificate completo :

for decode_path, obj, _ in CertificateList().decode_evgen(raw, ctx={
    "evgen_mode_upto": (
        (("tbsCertList", "revokedCertificates", any), True),
    ),
}):
    if (len(decode_path) == 3) and (decode_path[1] == "revokedCertificates"):
        print(int(obj["userCertificate"]))

Strings agregadas: agg_octet_string


Agora não temos problemas para decodificar objetos de qualquer tamanho. Não há problema em decodificar um CMS codificado em DER com uma cópia do banco de dados: estamos aguardando um evento com um caminho de decodificação apontando para dados assinados / criptografados no CMS e processando os dados do arquivo usando offset + vlen. Mas e se o CMS estivesse no formato CER? O deslocamento + vlen não ajudará, pois todos os nossos 10 GiBs são divididos em partes de 1000 bytes, entre os quais pelo cabeçalho DER. Mas e se tivermos um BER no qual o aninhamento de * STRINGs possa ser qualquer coisa?

SOME STRING[CONSTRUCTED]
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
    OCTET STRING[PRIMITIVE]
        DATA CHUNK
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[CONSTRUCTED]
            OCTET STRING[PRIMITIVE]
                DATA CHUNK

Ao decodificar no modo evgen, para cada peça obtemos o evento correspondente e é suficiente coletar apenas eventos primitivos (não construídos) * STRING , em que offset + vlen contém dados reais. O PyDERASN possui uma conveniente função auxiliar agg_octet_string que executa isso. É suficiente para ela passar um gerador de eventos, cujo caminho de decodificação "abaixo" é necessário para agregar uma string, dados binários (ou visão da memória ) e gravador - uma função que é chamada com cada dado recebido. Queremos calcular o hash SHA512 e, ao mesmo tempo, salvar o conteúdo do encapContentInfo.eContent CMS? Encontre a localização do campo de conteúdo (no qual SignedData estará localizado) e decodifique seu conteúdo de CER, gravando simultaneamente no FS e hash:

fdIn = open("data.p7m", "rb")
raw = file_mmaped(fdIn)
for decode_path, obj, _ in ContentInfo().decode_evgen(raw, ctx={"bered": True}):
    if decode_path == ("content",):
        content = obj
        break
hasher_state = sha512()
fdOut = open("dump.sql.zst", "wb")
def hash_n_save(data):
    write_full(fdOut, data)
    hasher_state.update(data)
    return len(data)
evgens = SignedData().decode_evgen(
    raw[content.offset:],
    offset=content.offset,
    ctx={"bered": True},
)
agg_octet_string(evgens, ("encapContentInfo", "eContent"), raw, hash_n_save)

Aqui, outro utilitário write_full é usado , que chama o writer até que todos os dados sejam gravados, pois, em geral, ao gravar em um arquivo, o sistema operacional não é necessário para processar todos os dados transferidos e você precisa continuar o processo até que ele seja completamente gravado.

Um par de carinhoso sobre SET OF


Tecnicamente, SET OF não pode ser codificado em tempo real nas codificações DER e CER, pois as representações codificadas de todos os elementos devem ser classificadas. Nem o CER nem o DER de duas passagens ajudarão aqui. O padrão ASN.1 moderno, portanto, não recomenda o uso de SET (que exige classificação semelhante no DER) e SET OF .

E qual é essa imagem no início do artigo?


Foi no PyDERASN que um navegador ASN.1 interativo e despretensioso apareceu , o que pessoalmente me ajudou várias vezes a escrever manipuladores para estruturas complexas. Permite percorrer toda a estrutura decodificada, mostrando detalhes completos sobre cada objeto, sua localização em dados binários, caminho de decodificação. Além disso, qualquer item pode ser salvo em um arquivo separado, como certificados ou CRLs incluídos no CMS.

Sergey Matveev , cipherpunk , desenvolvedor Python / Go, especialista chefe do FSUE "Centro Científico e Técnico" Atlas ".

All Articles