PyDERASN: como agregué soporte para big data

Continúo el último artículo sobre PyDERASN : el códec ASN.1 DER / CER / BER gratuito en Python. Durante el año pasado, desde el momento de su escritura, además de todas las pequeñas cosas, pequeñas correcciones y una verificación de datos aún más rigurosa (aunque antes era el códec libre más estricto que conozco), apareció en esta biblioteca una funcionalidad para trabajar con grandes volúmenes de datos. arrastrándose en la RAM. Quiero hablar de esto en este artículo.

Navegador ASN.1

Problemas / Tareas


  • 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 . .


¿Qué objetos ASN.1 en la práctica pueden consumir muchos recursos y ocupar mucho espacio en la memoria? Solo SECUENCIA DE / CONJUNTO DE que contiene numerosos objetos (cientos de miles en el caso de CACert.org), y todo tipo de * STRING (como en el segundo caso problemático). Como una de las virtudes de un programador es la pereza (según Larry Wall), intentaremos superar los problemas del consumo de recursos con un cambio mínimo en el código de la biblioteca.

* Los objetos STRING , desde el punto de vista del códec, casi no difieren entre sí y se implementan en la biblioteca mediante una clase común. ¿Qué es la codificación en OCTET STRING en DER?

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

Obviamente, el valor no tiene que estar en la RAM todo este tiempo. Se requiere que sea un objeto similar a bytes desde el que pueda averiguar la longitud.

memoryview satisface estas condiciones. Y, sin embargo, mmap puede hacer memoryview en cualquier archivo temporal que ya esté reducido en la memoria 20 GiB 10 GiB requisitos para las copias de la base de datos CMS a la mitad: es suficiente especificar como el valor de OCTET STRING s (o cualquier otro * STRING s) similar a memoryview . Para crearlo, PyDERASN tiene una función auxiliar:

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

Si queremos decodificar una gran cantidad de datos, memoryview también se puede utilizar para decodificar:

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

Consideramos que el problema con * STRING se ha resuelto parcialmente. Volver a crear una gran CRL. ¿Cómo es su estructura?

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

Una larga lista de pequeñas estructuras de RevokedCertificate . En este caso, contiene solo el número de serie del certificado y el momento de su revocación. ¿Cuál es su codificación DER?

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

Solo concatenación de representaciones DER de cada elemento individual de esta lista. ¿Necesitamos tener de antemano todos estos cientos de miles de objetos en el curso de ser serializados en 15 bytes de representación DER? Obviamente no. Por lo tanto, reemplazamos la lista de objetos con un iterador / generador. Lo más probable es que la creación de la CRL se base en datos del DBMS:

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

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

Ahora casi no consumimos memoria al crear una CRL de este tipo; solo necesitamos un lugar para trabajar con su representación DER: en este caso, estamos hablando de un par de decenas de megabytes (en oposición a una lista de medio gigabyte de objetos RevokedCertificate). Pero hay una diferencia en el comportamiento: la verificación de tamaño SECUENCIA DE / CONJUNTO DE se produce solo después de que el iterador se agota, y no en el momento de asignar un valor.

DER codificación de transmisión


Es imposible. Después de todo, esto es DER: ¡las longitudes de todos los elementos TLV (etiqueta + longitud + valor) deben conocerse de antemano!

¡Pero me gustaría mucho! Después de todo, todavía necesitamos 10 GiB de memoria para almacenar la representación DER de la copia de la base de datos: raw = cms.encode () ! Idealmente, quiero transmitir cierto escritor donde se escribiría la representación serializada. ¿Tal vez transferir el descriptor de archivo, dejar marcadores de posición en el archivo en el lugar de las longitudes y luego completarlos haciendo una búsqueda? Desafortunadamente, la longitud de la longitud (respectivamente, y el marcador de posición) tampoco se conocen de antemano.

PyDERASN ha recibido la posibilidad de codificación DER de dos pasos. En la primera pasada, se recopila conocimiento sobre la longitud de los objetos, creando un estado temporal. El segundo es streamingDER que codifica en un escritor determinado , ya posible debido al conocimiento de las longitudes. La implementación de esto resultó simple, simplemente agregando un poco de métodos de dos pasos a cada uno de los tipos base ASN.1. Dado que el recorrido de los objetos está estrictamente determinado (¡D - distinguido!), Luego, para almacenar las longitudes necesarias, se mantiene una lista simple, a la que, a medida que se recorre todo el árbol de objetos, se agregan valores de longitud. Para algunos tipos, la longitud es fija. Para algunos, solo es necesario para informar a un contenedor aguas arriba ( SECUENCIA / CONJUNTO , SECUENCIA DE / CONJUNTO DE , ETIQUETA EXPLÍCITA ). Por ejemplo, el estado de longitud para una lista de dos certificados revocados se ve así:

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

(42, [40, 18, 18])

En este caso, necesitamos conocer la longitud del valor solo para cada uno de los RevokedCertificate y la lista completa como un todo. La longitud de los enteros y el tiempo codificado (en DER es una longitud fija) en el estado no se almacena como innecesaria. Como resultado, para nuestra CRL CACert.org, dicha lista toma un poco más de 3.5 MiB, y para el CMS gigante, en el que casi todo el peso cae en un solo campo con una copia de la base de datos, toma aproximadamente 0.5 KiB.

La codificación de dos pasos se realiza mediante dos llamadas:

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

El primer paso también informa la longitud completa de los datos, que se pueden usar para verificar el espacio libre o asignarlo mediante alguna llamada posix_fallocate .

Usando la función auxiliar encode2pass (obj), puede realizar la codificación de dos pasos en la memoria. ¿Para qué? Puede ser significativamente más económico en el consumo de memoria, ya que, en el caso de CACert.org CRL, no almacenará 416k + pequeñas líneas binarias unidas por una llamada b "". Join () . Sin embargo, esto requiere más tiempo de procesador, ya que todos los objetos deberán caminar dos veces.

Ahora podemos codificar un CMS arbitrariamente grande en DER, prácticamente sin consumir memoria. Pero en el caso de CRL, utilizamos el generador de revocación de certificados, que se agotará después del final del primer pase. ¿Qué hacer? ¡Solo reinícielo de nuevo!

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

Por supuesto, estamos obligados a garantizar que el resultado del iterador sea exactamente el mismo, de lo contrario obtendremos un DER roto. Si los datos se toman del cursor DBMS, no se olvide del nivel y la clasificación de la transacción REPEATABLE READ .

Codificación de transmisión real: CER


Como sabe, DER (reglas de codificación distinguidas) es un subconjunto de BER (reglas de codificación básicas) que regula estrictamente las reglas de codificación de una y solo una forma. Esto le permite ser utilizado en tareas criptográficas. Pero hay otro subconjunto notable de BER: CER (reglas de codificación canónicas). Al igual que DER, solo tiene una representación posible de los datos. CER difiere de DER en varios detalles, pero le permiten realizar una verdadera codificación de transmisión de datos. Desafortunadamente, CER no se hizo tan popular como DER.

Omitiendo diferencias no tan notables (como ordenar etiquetas en SET), CER tiene dos diferencias fundamentales de 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
    

Todo esto (además de algunas pequeñas cosas) permite la transmisión (con 1000 bytes de un búfer para cadenas) para codificar cualquier objeto. Del mismo modo, puede usar mmap e iteradores. La codificación CER se realiza simplemente llamando al método .encode_cer (escritor) . Desafortunadamente, PyDERASN aún no puede verificar la validez de CER durante la decodificación, por lo que nos vemos obligados a decodificar los datos como BER.

El estándar CMS, por cierto, requiere codificación en BER (tanto DER como CER, automáticos, son BER). Por lo tanto, podemos codificar nuestra copia enorme de la base de datos en CER CMS sin un DER de dos pasos. Sin embargo , se requiere que SignedData tenga un elemento SignedAttributes codificado en DER, al igual que el Certificado X.509certificados PyDERASN le permite forzar el uso de DER en estructuras dadas simplemente agregando el atributo der_forced = True .

Decodificación de flujo: modo evgen


Aprendimos a codificar, pero solo mmap ayudará con la decodificación . Un decodificador de flujo "real", que tiene botones de control en forma de "dame más datos", "aquí estás", algún tipo de estado, requeriría una alteración radical de PyDERASN. Y, personalmente, no veo que sea más conveniente que la solución actual.

Y la solución actual es extremadamente simple. En el proceso de decodificación, tenemos en nuestras manos varios objetos primitivos decodificados en nuestras manos, a partir de ellos se ensamblan componentes superiores (construidos), de los cuales otros componentes, etc. ... Acumulamos objetos para hacerlos compuestos y, llegando a la parte superior danos un gran objeto. ¿Por qué no "dar" inmediatamente todo tipo de decodificaciónobjetos, tan pronto como aparecen en nuestras manos? Es decir, devolver no un objeto final, sino un generador que genera muchos objetos decodificados. De hecho, en PyDERASN ahora todos los métodos de decodificación se han convertido en generadores de tales "eventos" (generación de eventos, evgen).

Si habilitamos el modo de decodificación evgen de nuestra enorme CRL CACert.org, veremos la siguiente imagen:

$ 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

  • Al comienzo de la decodificación, vimos la etiqueta SECUENCIA de CertificateList , la longitud de los datos, pero el objeto aún no se sabe si puede decodificarse hasta el final. Hasta ahora solo estamos en el proceso de trabajar en ello.
  • SEQUENCE: version, INTEGER. . , . ( 10)
  • signature, SEQUENCE- : algorithm parameters. OBJECT IDENTIFIER ANY . ( 15, 26)
  • , , signature SEQUENCE , , : . ( 13)
  • , RevokedCertificate . ( 186, 206, ..)
  • tbsCertList . ( 5)
  • CertificateList, SEQUENCE, , , . ( 0)

Por supuesto, todos los * STRING y las listas ( * OF ) no tienen un significado real. En el caso de DER, conociendo .offset y .vlen, puede leer el valor de una línea de un archivo (¿un trozo de memoria?). Los objetos de secuencia se pueden recopilar y agregar según lo necesite, mientras recibe todos los eventos.

Ruta decodificada y evgen_mode_upto


¿Cómo entender qué tipo de objeto, qué tipo de INTEGER tenemos a mano? Cada objeto tiene su propia ruta de decodificación, que identifica de forma exclusiva un objeto específico en la estructura. Por ejemplo, para los eventos de ruta de decodificación de 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

Así es como podemos imprimir la lista de números de serie de revocación de certificados de esta 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))

La estructura RevokedCertificate puede contener mucha información, incluidas varias extensiones. Desde el punto de vista técnico, obtenemos todos los datos sobre el certificado revocado en modo evgen, pero no es muy conveniente agregar eventos relacionados con un elemento revokeado . Dado que cada Certificado Revoked final , en la práctica, no ocupará mucho espacio, sería genial tenerlo todo igual, no todos los objetos están tan "ordenados" por eventos. PyDERASN permite que la lista especifique rutas de decodificación en las que el modo evgen está deshabilitado. Por lo tanto, podemos establecer la ruta de decodificación (cualquier elemento de la lista ("tbsCertList", "revokedCertificates" ) en la que deseamos obtener el 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"]))

Cadenas agregadas: agg_octet_string


Ahora no tenemos problemas para decodificar objetos de cualquier tamaño. Tampoco hay problema en decodificar un CMS codificado con DER con una copia de la base de datos: estamos esperando un evento con una ruta de decodificación que apunte a los datos firmados / cifrados en el CMS y procesando los datos del archivo usando offset + vlen. Pero, ¿y si el CMS estuviera en formato CER? Entonces offset + vlen no ayudará, ya que todos nuestros 10 GiB se dividen en piezas de 1000 bytes, entre los cuales se encuentra el encabezado DER. Pero, ¿qué pasa si tenemos un BER en el que la anidación de * STRING puede ser cualquier cosa?

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

Al decodificar en modo evgen, para cada pieza obtenemos el evento correspondiente y es suficiente para nosotros recolectar solo eventos primitivos (no construidos) * STRING , donde offset + vlen contienen datos reales. PyDERASN tiene una conveniente función auxiliar agg_octet_string que realiza esto. Es suficiente para ella pasar un generador de eventos, la ruta de decodificación "debajo" de la cual es necesario agregar una cadena, datos binarios (o vista de memoria ) y un escritor , una función que se llama con cada pieza de datos recibida. Queremos calcular el hash SHA512 y al mismo tiempo guardar el contenido de encapContentInfo.eContent CMS? Encuentre la ubicación del campo de contenido (en el que se ubicará SignedData), y luego decodifica su contenido CER, escribiendo simultáneamente en el FS y hashing:

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)

Aquí, se utiliza otra utilidad write_full , que llama al escritor hasta que se escriben todos los datos, ya que, en general, cuando se escribe en un archivo, no se requiere que el sistema operativo procese todos los datos transferidos y debe continuar el proceso hasta que esté completamente escrito.

Un par de cariñosos sobre SET OF


Desde el punto de vista técnico, SET OF no puede codificarse sobre la marcha en las codificaciones DER y CER, ya que las representaciones codificadas de todos los elementos deben clasificarse. Ni CER ni un DER de dos pasos ayudarán aquí. El estándar ASN.1 moderno, por lo tanto, no recomienda el uso de SET (que requiere una clasificación similar en DER) y SET OF .

¿Y cuál es esta imagen al comienzo del artículo?


Fue en PyDERASN que apareció un navegador ASN.1 interactivo y sin pretensiones , que personalmente me ayudó varias veces a escribir controladores para estructuras complejas. Le permite caminar por toda la estructura decodificada, mostrando todos los detalles sobre cada objeto, su ubicación en datos binarios, ruta de decodificación. Además, cualquier elemento se puede guardar en un archivo separado, como certificados o CRL incluidos en el CMS.

Sergey Matveev , cipherpunk , Python / Go-developer, jefe de especialistas de FSUE "Centro Científico y Técnico" Atlas ".

All Articles