PyDERASN: como escribí la biblioteca ASN.1 con slots y blobs

ASN.1 es un est√°ndar (ISO, ITU-T, GOST) de un lenguaje que describe informaci√≥n estructurada, as√≠ como reglas de codificaci√≥n para esta informaci√≥n. Para m√≠, como programador, este es solo otro formato para serializar y presentar 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 En cuanto a la criptograf√≠a (X.509, CMS, est√°ndares PKCS), en tarjetas bancarias y pasaportes biom√©tricos, y mucho m√°s.

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

Mi propio

De hecho, no vale la pena recomendar ASN.1 para tareas criptogr√°ficas: ASN.1 y sus c√≥decs son complejos. Esto significa que el c√≥digo no ser√° simple, pero siempre es un vector de ataque adicional. Solo mire la lista de vulnerabilidades en las bibliotecas ASN.1. Bruce Schneier, en su ingenier√≠a de criptograf√≠a, tampoco recomienda usar este est√°ndar debido a su complejidad: "La codificaci√≥n TLV m√°s conocida es ASN.1, pero es incre√≠blemente compleja y nos alejamos de ella". Pero, desafortunadamente, hoy tenemos infraestructuras de clave p√ļblica en las que se utilizan activamente certificados X.509 , mensajes CRL, OCSP, TSP, CMP, CMC , CMS y muchos est√°ndares PKCS . Por lo tanto, debe poder trabajar con ASN.1 si est√° haciendo algo relacionado con la criptograf√≠a.

ASN.1 se puede codificar de varias formas / códecs:

  • BER (Reglas b√°sicas de codificaci√≥n)
  • 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 octeto)
  • PER (Reglas de codificaci√≥n empaquetadas)
  • SER (Reglas de codificaci√≥n espec√≠ficas de se√Īalizaci√≥n)
  • XER (Reglas de codificaci√≥n XML)

y varios otros. Pero en las tareas criptográficas en la práctica, se utilizan dos: BER y DER. Incluso en documentos XML firmados ( XMLDSig , XAdES ) todavía habrá objetos ASN.1 DER codificados en Base64, así como en el protocolo ACME basado en JSON de Let's Encrypt. Puede comprender mejor todos estos códecs y los principios de 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 Completo por el profesor John Larmouth .

BER es un formato TLV binario orientado a bytes (por ejemplo, PER, popular en comunicaciones móviles - orientado a bits). Cada elemento se codifica en forma de: una etiqueta ( T ag) que identifica el tipo de elemento que se codifica (entero, cadena, fecha, etc.), la longitud ( L ength) del contenido y el contenido en sí ( V alue). BER opcionalmente le permite no especificar un valor de longitud estableciendo un valor de longitud indefinido especial y terminando con un mensaje de Fin de Octetos. Además de la codificación de longitud, BER tiene mucha variabilidad en el método de codificación de tipos de datos, como:

  • INTEGER, IDENTIFICADOR DE OBJETO, BIT STRING y la longitud del elemento pueden no normalizarse (no codificarse en forma m√≠nima);
  • BOOLEAN es verdadero para cualquier contenido que no sea cero;
  • BIT STRING puede contener cero bits "extra";
  • BIT STRING, OCTET STRING y todos sus tipos de cadenas derivados, incluida la fecha / hora, se pueden dividir en partes (trozos) de longitud variable, cuya longitud durante la (des) codificaci√≥n no se conoce de antemano;
  • UTCTime / GeneralizedTime puede tener diferentes m√©todos para configurar el desplazamiento de zona horaria y las fracciones de cero "extra" de segundos;
  • Los valores de SECUENCIA PREDETERMINADA pueden o no estar codificados;
  • Los valores nombrados de los √ļltimos bits en BIT STRING pueden codificarse opcionalmente;
  • SECUENCIA (OF) / SET (OF) puede tener un orden arbitrario de elementos.

Para todo lo anterior, no siempre es posible codificar datos para que sean id√©nticos al formulario original. Por lo tanto, se invent√≥ un subconjunto de las reglas: DER es una regulaci√≥n estricta de un solo 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 durante la codificaci√≥n, lo que no permite la serializaci√≥n de datos. El c√≥dec CER no tiene este inconveniente, lo que garantiza una presentaci√≥n inequ√≠voca de los datos. Desafortunadamente (o afortunadamente, ¬Ņno tenemos decodificadores a√ļn m√°s complejos?), No se hizo popular. Por lo tanto, en la pr√°ctica, encontramos un uso "mixto" de datos codificados BER y DER. Dado que tanto CER como DER son un subconjunto de BER, cualquier decodificador BER es capaz de 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: estas son bibliotecas de muy bajo nivel que le permiten simplemente codificar / decodificar, por ejemplo, un entero y un encabezado de estructura, o esta es la biblioteca pyasn1 . Vivimos en √©l durante varios a√Īos y al principio est√°bamos muy satisfechos, ya que le permite trabajar con estructuras ASN.1 como objetos de alto nivel: por ejemplo, un objeto de certificado X.509 decodificado le permite acceder a sus campos a trav√©s de la interfaz del diccionario: cert ["tbsCertificate"] ["SerialNumber"] nos mostrar√° el n√ļmero de serie de este certificado. Del mismo modo, puede "recopilar" objetos complejos trabajando con ellos como con listas, diccionarios, y luego simplemente llamar a la funci√≥n pyasn1.codec.der.encoder.encode y obtener una representaci√≥n serializada del documento.

Sin embargo, se revelaron debilidades, problemas y limitaciones. Hubo, y desafortunadamente a√ļn persiste, errores en pyasn1: al momento de escribir, en pyasn1, uno de los tipos b√°sicos, GeneralizedTime, est√° decodificado y codificado incorrectamente .

En nuestros proyectos, para ahorrar espacio, a menudo almacenamos solo la ruta al archivo, el desplazamiento y la longitud en bytes del objeto al que queremos referirnos. Por ejemplo, un archivo firmado arbitrariamente se ubicar√° en la estructura ASN.1 SignedData de CMS:

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 original firmado con un desplazamiento de 65 bytes, 751 bytes de longitud. pyasn1 no almacena esta informaci√≥n en sus objetos decodificados. El llamado TLVSeeker fue escrito: una peque√Īa biblioteca que le permite decodificar las etiquetas y las longitudes de los objetos, en cuya interfaz le ordenamos "ir a la siguiente etiqueta", "ir dentro de la etiqueta" (ir dentro de la SECUENCIA del objeto), "ir a la siguiente etiqueta", "decirle a su desplazamiento y la longitud del objeto donde estamos ". Fue una caminata "manual" sobre los datos serializados DER de ASN.1. Pero no era posible trabajar con datos serializados BER de esta manera, porque, por ejemplo, la cadena de bytes OCTET STRING podr√≠a codificarse como varios fragmentos.

Otro inconveniente para nuestras tareas pyasn1 es la incapacidad de comprender a partir de objetos decodificados si un campo dado estaba presente en SECUENCIA o no. Por ejemplo, si la estructura contiene el campo OPCIONAL SECUENCIA DE CAMPO, podr√≠a estar completamente ausente de los datos recibidos (OPCIONAL), pero podr√≠a estar presente, pero podr√≠a ser de longitud cero (lista vac√≠a). En el caso general, esto no pudo aclararse. Y esto es necesario para una verificaci√≥n rigurosa de la validez de los datos recibidos. ¬°Imag√≠nese que alguna autoridad de certificaci√≥n emitir√≠a un certificado con datos "no enteramente" v√°lidos desde el punto de vista de los esquemas ASN.1! Por ejemplo, la autoridad de certificaci√≥n T√úRKTRUST Elektronik Sertifika Hizmet SańülayńĪcńĪsńĪ en su certificado ra√≠z super√≥ los l√≠mites permisibles de RFC 5280 para 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 DEFAULT no se codifique durante la transmisi√≥n; tales documentos se encuentran en la vida, y la primera versi√≥n de PyDERASN incluso permiti√≥ conscientemente ese comportamiento no v√°lido (desde el punto de vista de DER) en aras de la compatibilidad con versiones anteriores.

Otra limitación es la incapacidad de descubrir fácilmente en qué forma (BER / DER) se codificó uno u otro objeto en la estructura. Por ejemplo, el estándar CMS dice que el mensaje está codificado con BER, pero el campo firmadoAttrs, sobre el cual se forma la firma criptográfica, debe estar en DER. Si decodificamos con el DER, entonces recaeremos en el procesamiento del CMS en sí, si decodificamos con el BER, no sabremos en qué forma se firmó el atributo. Como resultado, será necesario que TLVSeeker (el análogo de los cuales no está en pyasn1) busque la ubicación de cada uno de los campos firmadosAttrs, y DER debe decodificarlo por separado desde la vista serializada.

La posibilidad de procesamiento automático de los campos DEFINED BY, que son muy comunes, fue muy deseable para nosotros. Después de decodificar la estructura ASN.1, es posible que nos queden muchos CUALQUIER campo, que deben 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 Python, esto significa escribir un if y luego llamar al decodificador para CUALQUIER campo.

El advenimiento de PyDERASN


En Atlas, regularmente, después de encontrar problemas o modificar los programas gratuitos utilizados, enviamos parches a la parte superior. En pyasn1, enviamos mejoras varias veces, pero el código pyasn1 no es el más fácil de entender, y a veces se produjeron cambios de API incompatibles, lo que nos golpeó en las manos. Además, estamos acostumbrados a escribir pruebas con pruebas generativas, que no era el caso en pyasn1.

¡Un buen día, decidí que tenía que soportar esto y era hora de intentar escribir mi propia biblioteca con __slot __s, offset y blobs bellamente exhibidos! Simplemente crear un códec ASN.1 no sería suficiente: debe transferirle todos nuestros proyectos dependientes, y esto es cientos de miles de líneas de código en las que hay mucho trabajo con estructuras ASN.1. Ese es uno de los requisitos para ello: facilidad de traducción del código pyasn1 actual. Después de pasar todas mis vacaciones, escribí esta biblioteca, le transferí todos los proyectos. Dado que tienen una cobertura de pruebas de casi el 100%, esto también significaba que la biblioteca estaba en pleno funcionamiento.

PyDERASN, del mismo modo, tiene casi el 100% de cobertura de prueba. Las pruebas generativas se utilizan con la maravillosa biblioteca de hip√≥tesis . Tambi√©n se llev√≥ a cabo un fuzzing pypy en 32 m√°quinas nucleares. A pesar de que casi no nos queda ning√ļn c√≥digo Python2, PyDERASN a√ļn mantiene la compatibilidad con √©l y, debido a esto, tiene una sola dependencia de seis . Adem√°s, se prueba con el conjunto de pruebas de cumplimiento ASN.1: 2008 .

El principio de trabajar con él es similar a pyasn1: trabajar con objetos Python de alto nivel. La descripción de los circuitos 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 tipeo fuerte. En pyasn1, si el campo era de tipo CMSVersion (INTEGER), se le podría asignar un int o INTEGER. PyDERASN requiere estrictamente que el objeto asignado sea exactamente CMSVersion. Además de escribir código Python3, utilizamos anotaciones de tipeo , por lo que nuestras funciones no tendrán argumentos incomprensibles como def func (serial, contenido), pero def func (serial: CertificateSerialNumber, contenido: EncapsulatedContentInfo), y PyDERASN ayuda a realizar un seguimiento de tales código

Al mismo tiempo, PyDERASN tiene concesiones extremadamente convenientes para este tipo de escritura. pyasn1 no permitió que SubjectKeyIdentifier (). subtype (impllicitTag = Tag (...)) asignara un objeto SubjectKeyIdentifier () (sin la ETIQUETA IMPLICIT requerida) y a menudo tuvo que copiar y recrear objetos solo debido a las etiquetas IMPLICIT / EXPLICIT cambiadas. PyDERASN observa estrictamente solo el tipo básico: sustituirá automáticamente las etiquetas de una estructura ASN.1 existente. Esto simplifica enormemente el código de la aplicación.

Si se produce un error durante la decodificaci√≥n, en pyasn1 no es f√°cil entender exactamente d√≥nde ocurri√≥. Por ejemplo, en el certificado turco ya mencionado obtenemos este error: UTF8String (tbsCertificate: issuer: rdnSequence: 3: 0: value: DEFINED BY 2.5.4.10:utf8String) (at 138) l√≠mites insatisfechos: 1 ‚áź 77 ‚áź 64 Al escribir ASN .1 las estructuras de las personas pueden cometer errores, y es m√°s f√°cil depurar aplicaciones o descubrir problemas de documentos codificados del lado opuesto.

La primera versi√≥n de PyDERASN no era compatible con la codificaci√≥n BER. Apareci√≥ mucho m√°s tarde y el procesamiento UTCTime / GeneralizedTime con zonas horarias a√ļn no es compatible. Esto vendr√° en el futuro, porque el proyecto est√° escrito principalmente en el tiempo libre.

También en la primera versión no hubo trabajo con los campos DEFINED BY. Unos meses más tarde, apareció esta oportunidad y comenzó a utilizarse activamente, lo que redujo significativamente el código de la aplicación: en una operación de decodificación, fue posible desmontar toda la estructura a la profundidad misma. Para hacer esto, en el esquema, qué campos están definidos que "determinan". Por ejemplo, una descripción de un 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 id_signedData, entonces el campo de contenido (ubicado en la misma SECUENCIA) debe decodificarse utilizando el esquema SignedData. ¬ŅPor qu√© hay tantos corchetes? Un campo puede "definir" varios campos al mismo tiempo, como es el caso en las estructuras de EnvelopedData. Los campos definidos se identifican mediante la llamada ruta de decodificaci√≥n: establece la ubicaci√≥n exacta de cualquier elemento en todas las estructuras.

No siempre es deseable o no siempre es posible introducir inmediatamente estas definiciones en el circuito. 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 especificar 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 CMS SignedData para todos los certificados adjuntos, decodifica todas sus extensiones (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool, etc.). A través de la ruta de decodificación, indicamos qué elemento "sustituir" define, como si estuviera definido en el circuito.

Finalmente, PyDERASN tiene la capacidad de trabajar desde la línea de comandos para decodificar archivos ASN.1 y tiene una impresión bastante rica. Puede decodificar un ASN.1 arbitrario, o puede especificar un esquema claramente definido y ver algo como esto:

Bonito ejemplo de impresión

Informaci√≥n mostrada: desplazamiento de objeto, longitud de etiqueta, longitud de longitud, longitud de contenido, presencia de EOC (fin de octetos), bandera de codificaci√≥n BER, bandera de codificaci√≥n de longitud indefinida, longitud de etiqueta EXPLICIT y compensaci√≥n (si existe), profundidad de anidamiento de objeto en estructuras, valor de etiqueta IMPLICIT / EXPLICIT, nombre de objeto de acuerdo con el esquema, su tipo ASN.1 b√°sico, n√ļmero de serie dentro de SECUENCIA / CONJUNTO, valor CHOICE (si lo hay), nombre legible para humanos INTEGER / ENUMERATED / BIT STRING de acuerdo con el esquema, valor de cualquier tipo b√°sico , Indicador POR DEFECTO / OPCIONAL del circuito, una se√Īal de que el objeto fue decodificado autom√°ticamente como DEFINIDO POR y despu√©s gm de OID-y sucedi√≥, OID chelovekochitaemy.

El bonito sistema de impresión está hecho especialmente para generar una secuencia de objetos PP que ya se visualizan por medios separados. La captura de pantalla muestra el renderizador en texto plano. Hay renderizadores en formato JSON / HTML para que esto se pueda ver resaltado en el navegador ASN.1 como en el proyecto asn1js .

Otras bibliotecas


Este no era el objetivo, pero PyDERASN fue significativamente m√°s r√°pido que pyasn1. Por ejemplo, decodificar archivos CRL de megabytes puede llevar tanto tiempo que tenga que pensar en formatos intermedios para almacenar datos (r√°pido) y cambiar la arquitectura de las aplicaciones. pyasn1 decodifica CRL CACert.org en mi computadora port√°til durante m√°s de 20 minutos, ¬°mientras que PyDERASN en solo 28 segundos! Hay un proyecto asn1crypto destinado a trabajar r√°pidamente con estructuras criptogr√°ficas: decodifica (completamente, no perezosamente) la misma CRL en 29 segundos, pero consume casi el doble de RAM cuando se ejecuta en Python3 (983 MiB versus 498), y 3.5 veces con Python2 (1677 contra 488), mientras que pyasn1 consume hasta 4.3 veces m√°s (2093 contra 488).

asn1crypto, que mencion√©, no lo consideramos, porque el proyecto estaba reci√©n en pa√Īales y no hab√≠amos o√≠do hablar de √©l. Ahora tampoco comenzar√≠an a mirar en su direcci√≥n, ya que descubr√≠ de inmediato que el mismo GeneralizedTime no tiene un aspecto arbitrario, y al serializar, elimina silenciosamente fracciones de un segundo. Esto es aceptable para trabajar con certificados X.509, pero en general no funcionar√°.

Por el momento, PyDERASN es el decodificador Py DER / Go DER más estricto que conozco. En la biblioteca de codificación / asn1 de mi Go favorito, no hay una verificación estricta de las cadenas IDENTIFICADOR DE OBJETO y UTCTime / GeneralizedTime. A veces, la rigidez puede interferir (principalmente debido a la compatibilidad con aplicaciones anteriores que nadie solucionará), por lo que en PyDERASN durante la decodificación puede pasar varias configuraciones que debilitan las verificaciones.

El código del proyecto intenta ser lo más simple posible. Toda la biblioteca es un solo archivo. El código está escrito con énfasis en la facilidad de comprensión, sin rendimiento innecesario y optimizaciones de código DRY. Como ya dije, no admite la decodificación BER completa de las cadenas UTCTime / GeneralizedTime, así como 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.

Al igual que todos mis proyectos, como PyGOST , GoGOST , NNCP , GoVPN , PyDERASN es un software completamente gratuito distribuido bajo los términos de LGPLv3 + , y está disponible para su descarga gratuita. Ejemplos de uso están aquí en las pruebas PyGOST .

Sergey Matveev , banco de cifrado , miembro de la Open Society Foundation Foundation , Python / Go-developer, jefe especialista de FSUE "STC Atlas" .

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


All Articles