PyDERASN: cómo escribí una biblioteca ASN.1 con ranuras y blobs

ASN.1 este es un estándar (ISO, ITU-T, GOST) para un lenguaje que describe información estructurada, así como reglas para codificar esta información. Para mí, como programador, este es solo otro formato de serialización y presentación de datos, junto con JSON, XML, XDR y otros. Es extremadamente común en nuestra vida cotidiana, y muchas personas lo encuentran: en comunicaciones celulares, telefónicas, VoIP (UMTS, LTE, WiMAX, SS7, H.323), en protocolos de red (LDAP, SNMP, Kerberos), en todo lo que se refiere a la criptografía (estándares X.509, CMS, PKCS), en tarjetas bancarias y pasaportes biométricos, y en muchos otros lugares.

Este artículo discute PyDERASN: biblioteca Python ASN.1 utilizada activamente en proyectos relacionados con la criptografía en Atlas.

PyDERASN: cómo escribí una biblioteca ASN.1 con ranuras y blobs
En general, ASN.1 no se recomienda para tareas criptográficas: ASN.1 y sus códecs son complejos. Esto significa que el código no será simple, y esto siempre es un vector de ataque extra. suficiente para ver a la lista vulnerabilidades en bibliotecas ASN.1. Bruce Schneier en su Ingeniería criptográfica también desaconseja el uso de este estándar debido a su complejidad: "La codificación TLV más conocida es ASN.1, pero es increíblemente compleja y la evitamos". Pero, lamentablemente, hoy tenemos infraestructuras de clave publica que usan activamente Certificados X.509, CRL, OCSP, TSP, protocolos CMP, CMC, mensajes CMSy muchas normas PKCS. Por lo tanto, debe poder trabajar con ASN.1 si está haciendo algo relacionado con la criptografía.

ASN.1 se puede codificar en una variedad de formas/códecs:

  • BER (Reglas de codificación básicas)
  • CER (Reglas de codificación canónica)
  • DER (Reglas de codificación distinguidas)
  • GSER (Reglas de codificación de cadenas genéricas)
  • JER (Reglas de codificación JSON)
  • LWER (Reglas de codificación de peso ligero)
  • REA (Reglas de codificación de octetos)
  • (Reglas de codificación empaquetadas)
  • SER (Reglas de codificación específicas de señalización)
  • XER (Reglas de codificación XML)

y un número de otros. Pero en tareas criptográficas, en la práctica se utilizan dos: BER y DER. Incluso en documentos XML firmados (XMLDSig, XADES) seguirán siendo objetos ASN.64 DER codificados en Base1, al igual que en un protocolo orientado a JSON CUMBRE de Let´s Encrypt. Puede comprender mejor todos estos códecs y los principios de la codificación BER / CER / DER en artículos y libros: ASN.1 en palabras simples, ASN.1 - Comunicación entre sistemas heterogéneos por Olivier Dubuisson, ASN.1 Completado por el profesor John Larmouth.

BER es un formato TLV binario orientado a bytes (por ejemplo, PER, popular en la comunicación celular, orientado a bits). Cada elemento está codificado como: etiqueta (Tag) identificando el tipo de elemento que se codifica (entero, cadena, fecha, etc.), longitud (Llongitud) del contenido y el contenido mismo (Vvalor). BER opcionalmente permite que se omita un valor de longitud proporcionando un valor de longitud indefinido especial y finalizando el mensaje de fin de octeto con una etiqueta. Además de la codificación de longitud, BER tiene muchas variaciones en la forma en que se codifican los tipos de datos, como:

  • INTEGER, OBJECT IDENTIFIER, BIT STRING y la longitud del elemento pueden desnormalizarse (no codificarse mínimamente);
  • BOOLEAN es verdadero para cualquier contenido no nulo;
  • BIT STRING puede contener bits cero "extra";
  • CADENA DE BITS, CADENA DE OCTETOS y todos sus tipos de cadenas derivadas, incluida la fecha/hora, se pueden dividir en fragmentos (trozo) de longitud variable, cuya longitud no se conoce de antemano durante la (des)codificación;
  • UTCTime/GeneralizedTime puede tener diferentes formas de establecer el desplazamiento de la zona horaria y las fracciones de segundo cero "adicionales";
  • los valores DEFAULT SEQUENCE pueden o no estar codificados;
  • Los valores con nombre de los últimos bits en una CADENA DE BITS se pueden dejar opcionalmente sin codificar;
  • SEQUENCE (OF)/SET (OF) puede tener un orden arbitrario de elementos.

Por todo lo anterior, no siempre es posible codificar los datos para que sean idénticos a la forma original. Por lo tanto, se inventó un subconjunto de reglas: DER: regula estrictamente solo un método de codificación válido, que es fundamental para las tareas criptográficas, donde, por ejemplo, cambiar un bit invalidará la firma o la suma de verificación. DER tiene un inconveniente significativo: las longitudes de todos los elementos deben conocerse de antemano en el momento de la codificación, lo que no permite la serialización de flujo de datos. El códec CER está libre de esta deficiencia, lo que garantiza de manera similar una representación inequívoca de los datos. Desafortunadamente (¿o afortunadamente no tenemos decodificadores aún más sofisticados?), no se hizo popular. Por lo tanto, en la práctica nos encontramos con un uso "mixto" de datos codificados BER y DER. Dado que tanto CER como DER son un subconjunto de BER, cualquier decodificador de BER puede procesarlos.

Problemas con pyasn1

En el trabajo, escribimos muchos programas de Python relacionados con la criptografía. Y hace unos años prácticamente no había elección de bibliotecas gratuitas: o estas son bibliotecas de muy bajo nivel que le permiten simplemente codificar / decodificar, por ejemplo, un número entero y un encabezado de estructura, o esta es una biblioteca pyasn1. Vivimos durante varios años y al principio estábamos muy contentos, ya que te permite trabajar con estructuras ASN.1 como con objetos de alto nivel: por ejemplo, un objeto de certificado X.509 decodificado te permite acceder a sus campos a través de un interfaz de diccionario: cert["tbsCertificate"] ["serialNumber"] nos mostrará el número de serie de este certificado. De manera similar, puede "recopilar" objetos complejos trabajando con ellos como si fueran listas, diccionarios y luego simplemente llame a la función pyasn1.codec.der.encoder.encode y obtenga una representación serializada del documento.

Sin embargo, se revelaron deficiencias, problemas y limitaciones. Hubo y, desafortunadamente, todavía quedan errores en pyasn1: en el momento de escribir esto, en pyasn1 uno de los tipos básicos es GeneralizedTime, incorrectamente decodificado y codificado.

En nuestros proyectos, para ahorrar espacio, a menudo solo almacenamos la ruta del archivo, el desplazamiento y la longitud en bytes del objeto al que queremos hacer referencia. Por ejemplo, lo más probable es que un archivo firmado arbitrariamente se encuentre en una estructura 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

y podemos obtener el archivo firmado original con un desplazamiento de 65 bytes, con una longitud de 751 bytes. pyasn1 no almacena esta información en sus objetos decodificados. Se escribió el llamado TLVSeeker, una pequeña biblioteca que le permite decodificar etiquetas y longitudes de objetos, en cuya interfaz ordenamos "ir a la siguiente etiqueta", "ir dentro de la etiqueta" (ir dentro de la SECUENCIA del objeto), "ir a la siguiente etiqueta", "decir su desplazamiento y la longitud del objeto donde estamos". Este fue un recorrido "manual" de datos serializados ASN.1 DER. Pero era imposible trabajar con datos serializados por BER de esta manera, ya que, por ejemplo, la cadena de bytes OCTET STRING podría codificarse como varios fragmentos.

Otra desventaja para nuestras tareas pyasn1 es la incapacidad de comprender a partir de los objetos decodificados si el campo dado estaba presente en la SECUENCIA o no. Por ejemplo, si la estructura contiene el campo Field SEQUENCE OF Smth OPTIONAL, entonces podría estar completamente ausente en los datos entrantes (OPTIONAL), o podría estar presente, pero al mismo tiempo ser de longitud cero (una lista vacía). En el caso general, esto no se pudo averiguar. Y esto es necesario para una verificación estricta de la validez de los datos entrantes. ¡Imagínese que alguna autoridad de certificación emitiera un certificado con datos "no del todo" válidos desde el punto de vista de los esquemas ASN.1! Por ejemplo, el centro de certificación "TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı" en su certificado raíz fue más allá de lo permitido RFC 5280 límites en la longitud del componente sujeto: no se puede decodificar honestamente de acuerdo con el esquema. El códec DER requiere que un campo cuyo valor sea igual a DEFAULT no se codifique durante la transmisión; dichos documentos se encuentran en la vida, y la primera versión de PyDERASN incluso permitió deliberadamente tal comportamiento inválido (desde el punto de vista de DER) por el bien de retrocompatibilidad.

Otra limitación es la incapacidad de averiguar fácilmente en qué forma (BER / DER) se codificó este o aquel objeto en la estructura. Por ejemplo, el estándar CMS dice que el mensaje está codificado en BER, pero el campo SignedAttrs, sobre el cual se forma la firma criptográfica, debe estar en DER. Si decodificamos con DER, caeremos en el procesamiento del propio CMS, si decodificamos con BER, entonces no sabremos en qué forma estaban los attrs firmados. Como resultado, TLVSeeker (cuyo análogo no está en pyasn1) deberá buscar la ubicación de cada uno de los campos SignedAttrs y decodificarlo por separado, tomándolo de la representación serializada, con DER.

La capacidad de procesar automáticamente campos DEFINED BY, que son muy comunes, fue muy bienvenida para nosotros. Después de decodificar una estructura ASN.1, es posible que nos quede un conjunto de CUALQUIER campo que debe procesarse más de acuerdo con el esquema seleccionado en función del IDENTIFICADOR DE OBJETO especificado en el campo de estructura. En el código de Python, esto significa escribir si y luego llamar al decodificador para el campo ANY.

El surgimiento de PyDERASN

En el Atlas, enviamos parches con regularidad cuando detectamos problemas o mejoramos el software gratuito que utilizamos. En pyasn1, presentamos mejoras varias veces, pero el código de pyasn1 no es el más fácil de entender y, a veces, hubo cambios de API incompatibles que nos afectaron. Además, estamos acostumbrados a escribir pruebas con pruebas generativas, que no era el caso en pyasn1.

Un buen día, decidí que era suficiente para soportarlo y que era hora de intentar escribir mi propia biblioteca con __slot__s, compensaciones y blobs hermosamente representados. No sería suficiente simplemente crear un códec ASN.1: necesitamos transferirle todos nuestros proyectos dependientes, y estos son cientos de miles de líneas de código que están llenas de trabajo con estructuras ASN.1. Es decir, uno de los requisitos para ello: la facilidad de traducción del código pyasn1 actual. Después de pasar todas mis vacaciones, escribí esta biblioteca, transferí todos los proyectos a ella. Dado que tienen una cobertura de casi el 100% de las pruebas, esto significaba que la biblioteca era completamente funcional.

PyDERASN, de manera similar, tiene una cobertura de prueba de casi el 100%. Utiliza pruebas generativas con una biblioteca maravillosa. hipótesis. También celebrada y fuzzing py-afl-comer en 32 máquinas nucleares. A pesar de que prácticamente no nos queda código de Python2, PyDERASN todavía observa compatibilidad con él y por eso tiene el único seis adiccion. Además, ha sido probado contra ASN.1: conjunto de pruebas de cumplimiento de 2008.

El principio de trabajar con él es similar a pyasn1: trabajar con objetos Python de alto nivel. La descripción de los esquemas ASN.1 es similar.

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)),
    )

Sin embargo, PyDERASN tiene una apariencia de tipificación fuerte. En pyasn1, si un campo era del tipo CMSVersion(INTEGER), entonces podría asignarse int o INTEGER. PyDERASN requiere estrictamente que el objeto asignado sea exactamente CMSVersion. Además de escribir código Python3, también usamos escribiendo anotaciones, por lo que nuestras funciones no tendrán argumentos oscuros como def func(serial, contenidos), pero def func(serial: CertificateSerialNumber, contenidos: EncapsulatedContentInfo), y PyDERASN ayuda a observar dicho código.

Al mismo tiempo, PyDERASN tiene indulgencias extremadamente convenientes para esta escritura. pyasn1 no permitía en el campo SubjectKeyIdentifier().subtype(implicitTag=Tag(…)) asignar un objeto al SubjectKeyIdentifier() (sin la ETIQUETA IMPLÍCITA requerida) y, a menudo, tenía que copiar y recrear objetos solo debido a cambios IMPLÍCITOS/ Etiquetas EXPLÍCITAS. PyDERASN observa estrictamente solo el tipo base: sustituirá automáticamente las etiquetas del esquema ASN.1 ya existente de la estructura. Esto simplifica enormemente el código de la aplicación.

Si ocurre un error durante la decodificación, no es fácil para pyasn1 entender exactamente dónde ocurrió. Por ejemplo, en el certificado turco ya mencionado anteriormente, obtendremos el siguiente error: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (en 138) límites no satisfechos: 1 ⇐ 77 ⇐ 64 Al escribir estructuras ASN .1, las personas pueden cometer errores y ayuda a depurar aplicaciones más fácilmente o resolver problemas en documentos codificados del otro lado.

La primera versión de PyDERASN no admitía la codificación BER. Apareció mucho más tarde y aún no es compatible con el procesamiento UTCTime/GeneralizedTime con zonas horarias. Esto vendrá en el futuro, porque el proyecto se escribe principalmente en su tiempo libre.

Además, en la primera versión no se trabajaba con campos DEFINIDO POR. Unos meses después este surgió la oportunidad y comenzó a usarse activamente, reduciendo significativamente el código de la aplicación: en una operación de decodificación, fue posible desmontar toda la estructura hasta el fondo. Para ello, en el esquema, qué campos “definen” lo que se configura. Por ejemplo, una descripción del esquema 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))),
    )

dice que si contentType contiene un OID con un valor de id_signedData, entonces el campo de contenido (ubicado en la misma SECUENCIA) debe decodificarse de acuerdo con el esquema SignedData. ¿Por qué tantos corchetes? Un campo puede "definir" varios campos al mismo tiempo, como es el caso de las estructuras EnvelopedData. Los campos definidos se identifican mediante la llamada ruta de decodificación: especifica la ubicación exacta de cualquier elemento en todas las estructuras.

No siempre es deseable o no siempre es posible agregar inmediatamente estas definiciones al esquema. Puede haber casos específicos de aplicaciones en los que los OID y las estructuras solo se conocen en un proyecto de terceros. PyDERASN proporciona la capacidad de establecer estas definiciones justo en el momento de decodificar la estructura:

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(),
    }),),
),)})

Aquí decimos que en el CMS SignedData para todos los certificados adjuntos, decodifique todas sus extensiones (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool, etc.). Indicamos a través de la ruta de decodificación qué elemento debe ser "sustituido" con defines, como si estuviera establecido en el esquema.

Finalmente, PyDERASN tiene la capacidad de trabajar desde línea de comando para decodificar archivos ASN.1 y tiene un rico bonita impresión. Puede decodificar un ASN.1 arbitrario, o puede establecer un esquema bien definido y ver algo como esto:

PyDERASN: cómo escribí una biblioteca ASN.1 con ranuras y blobs

Información mostrada: desplazamiento del objeto, longitud de la etiqueta, longitud de la longitud, longitud del contenido, presencia de EOC (fin de octeto), indicador de codificación BER, indicador de codificación de longitud indefinida, longitud y desplazamiento de la etiqueta EXPLÍCITO (si corresponde), profundidad de anidamiento del objeto en estructuras, valor de etiqueta IMPLÍCITO/EXPLICITO, nombre de esquema del objeto, su tipo base ASN.1, número de secuencia dentro de SEQUENCE/SET OF, valor de CHOICE (si lo hay), nombre de esquema legible por humanos INTEGER/ENUMERADO/BIT STRING, valor de cualquier tipo base, marca DEFAULT/OPTIONAL del esquema, una señal de que el objeto se decodificó automáticamente como DEFINIDO POR y debido a qué OID sucedió esto, un OID legible por humanos.

El bonito sistema de impresión está hecho especialmente de tal manera que genera una secuencia de objetos PP que ya están renderizados por medios separados. La captura de pantalla muestra el renderizador en texto de color sin formato. También hay renderizadores en formato JSON/HTML para que se pueda ver resaltado en el navegador ASN.1 como en asn1js proyecto.

Otras bibliotecas

Este no era el objetivo, pero PyDERASN resultó ser significativo más rápido que pyasn1. Por ejemplo, la decodificación de archivos CRL de tamaños de megabytes puede llevar tanto tiempo que debe pensar en formatos de almacenamiento de datos intermedios (rápidos) y cambiar la arquitectura de las aplicaciones. pyasn1 decodifica la CRL CACert.org en mi computadora portátil durante más de 20 minutos, mientras que PyDERASN toma solo 28 segundos. hay un proyecto asn1cripto, destinado al trabajo rápido con estructuras criptográficas: decodifica (completamente, no con pereza) la misma CRL en 29 segundos, pero consume casi el doble de RAM cuando se ejecuta bajo Python3 (983 MiB versus 498), y en 3.5 veces bajo Python2 (1677 vs 488), mientras que pyasn1 consume hasta 4.3 veces más (2093 vs 488).

asn1crypto, que mencioné, no lo consideramos porque el proyecto aún estaba en pañales y no habíamos oído hablar de él. Ahora tampoco mirarían en su dirección, ya que inmediatamente descubrí que el mismo GeneralizedTime no toma una forma arbitraria, y durante la serialización elimina silenciosamente una fracción de segundo. Esto es aceptable para trabajar con certificados X.509, pero en general no funcionará.

Por el momento, PyDERASN es el decodificador Python/Go DER gratuito más riguroso que conozco. En la biblioteca de codificación/asn1 de mi Go favorito sin control estricto IDENTIFICADOR DE OBJETO y cadenas UTCTime/GeneralizedTime. A veces, el rigor puede interponerse (principalmente debido a la compatibilidad con versiones anteriores de aplicaciones antiguas que nadie arreglará), por lo que en PyDERASN durante la decodificación, puede pasar varias configuraciones debilitamiento de cheques.

El código del proyecto intenta ser lo más simple posible. Toda la biblioteca es un archivo. El código está escrito con énfasis en la facilidad de comprensión, sin optimizaciones de rendimiento excesivas ni código DRY. Como ya dije, no es compatible con la decodificación BER completa de las cadenas UTCTime / GeneralizedTime, así como con los tipos de datos REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING. En todos los demás casos, personalmente no veo ninguna razón para usar otras bibliotecas en Python.

Como todos mis proyectos, como PyGOST, IrGOST, NNCP, GoVPN, PyDERASN es completamente software libredistribuido bajo los términos LGPLv3+, y está disponible para su descarga gratuita. Hay ejemplos de uso. aquí y Pruebas PyGOST.

Sergey Matveev, ciberpunkmiembro Fundación SPO, desarrollador Python/Go, especialista jefe Empresa Unitaria del Estado Federal "STC "Atlas".

Fuente: habr.com

Añadir un comentario