PyDERASN: como escribín unha biblioteca ASN.1 con slots e blobs

ASN.1 este é un estándar (ISO, ITU-T, GOST) dunha linguaxe que describe información estruturada, así como regras para codificar esta información. Para min, como programador, este é só outro formato para serializar e presentar datos, xunto con JSON, XML, XDR e outros. É extremadamente común na nosa vida cotiá, e moitas persoas atópanse con el: en comunicacións móbiles, telefónicas, VoIP (UMTS, LTE, WiMAX, SS7, H.323), en protocolos de rede (LDAP, SNMP, Kerberos), en todo o que preocupa a criptografía (X.509, CMS, estándares PKCS), en tarxetas bancarias e pasaportes biométricos, e moito máis.

Este artigo trata sobre PyDERASN: A biblioteca Python ASN.1 úsase activamente en proxectos relacionados coa criptografía en Atlas.

PyDERASN: como escribín unha biblioteca ASN.1 con slots e blobs
En xeral, ASN.1 non paga a pena recomendar para tarefas criptográficas: ASN.1 e os seus códecs son complexos. Isto significa que o código non será sinxelo, e este sempre é un vector de ataque adicional. Só mira á lista vulnerabilidades nas bibliotecas ASN.1. Bruce Schneier no seu Enxeñaría criptográfica tamén desaconsella o uso deste estándar debido á súa complexidade: "A codificación TLV máis coñecida é ASN.1, pero é incriblemente complexa e evitámola". Pero, por desgraza, hoxe temos infraestrutura de chave pública nos que se utilizan activamente Certificados X.509protocolos , CRL, OCSP, TSP, CMP, CMC, mensaxes CMS, e moitos estándares PKCS. Polo tanto, tes que poder traballar con ASN.1 se estás facendo algo relacionado coa criptografía.

ASN.1 pódese codificar de varias formas/códecs:

  • Ber (Regras básicas de codificación)
  • CER (Regras de codificación canónicas)
  • O (Regras de codificación distinguidas)
  • GSER (Regras de codificación de cadeas xenéricas)
  • JER (Regras de codificación JSON)
  • LWER (Regras de codificación de peso lixeiro)
  • NOSA (Regras de codificación de octetos)
  • PARA (Regras de codificación empaquetadas)
  • SER (Regras de codificación específicas de sinalización)
  • DISCÍPULOS (Regras de codificación XML)

e unha serie de outros. Pero nas tarefas criptográficas, na práctica, utilízanse dúas: BER e DER. Incluso en documentos XML asinados (XMLDSig, XAdES) aínda haberá obxectos DER ASN.64 codificados en Base1, como no protocolo orientado a JSON acme de Let's Encrypt. Podes comprender mellor todos estes códecs e principios de codificación BER/CER/DER en artigos e libros: ASN.1 en palabras sinxelas, ASN.1 — Comunicación entre sistemas heteroxéneos por Olivier Dubuisson, ASN.1 Completado polo profesor John Larmouth.

BER é un formato TLV binario orientado a bytes (por exemplo PER, popular en comunicacións móbiles - orientado a bits). Cada elemento está codificado como: etiqueta (Tag), identificando o tipo de elemento a codificar (número enteiro, cadea, data, etc.), lonxitude (Length) contido e o propio contido (Valo). Opcionalmente, BER permítelle non especificar un valor de lonxitude establecendo un valor especial de lonxitude indefinido e rematando a mensaxe de Fin de octetos cunha marca de Fin de octetos. Ademais da codificación de lonxitude, BER ten moita variabilidade na forma en que codifica os tipos de datos, como:

  • É posible que o enteiro, o identificador de obxecto, a cadea de bits e a lonxitude do elemento non estean normalizados (non se codifiquen de forma mínima);
  • BOOLEAN é verdadeiro para calquera contido distinto de cero;
  • BIT STRING pode conter cero bits "extra";
  • BIT STRING, OCTET STRING e todos os seus tipos de cadea derivadas, incluída a data/hora, pódense dividir en anacos de lonxitude variable, cuxa lonxitude non se coñece de antemán no momento da (des)codificación;
  • UTCTime/XeneralizedTime pode ter diferentes formas de especificar a compensación da zona horaria e fraccións de segundo cero "extra";
  • Os valores da SECUENCIA PREDETERMINADA poden ser codificados ou non;
  • Os valores nomeados dos últimos bits nunha cadea de bits pódense descodificar opcionalmente;
  • SEQUENCE (OF)/SET (OF) pode ter calquera orde de elementos.

Por todo o anterior, non sempre é posible codificar os datos para que sexan idénticos á forma orixinal. Polo tanto, inventouse un subconxunto de regras: DER - que regula estrictamente un só método de codificación válido, o que é fundamental para tarefas criptográficas onde, por exemplo, cambiar un bit fará que a sinatura ou a suma de verificación non sexan válidas. DER ten unha desvantaxe importante: as lonxitudes de todos os elementos deben coñecerse de antemán no momento da codificación, o que non permite a serialización do fluxo de datos. O códec CER non ten este inconveniente, garantindo igualmente unha representación inequívoca dos datos. Desafortunadamente (ou é unha sorte que non teñamos decodificadores aínda máis complexos?), non se fixo popular. Polo tanto, na práctica atopamos un uso "mixto" de datos codificados BER e DER. Dado que tanto CER como DER son un subconxunto de BER, calquera descodificador de BER pode manexalos.

Problemas con pyasn1

No traballo escribimos moitos programas de Python relacionados coa criptografía. E hai uns anos practicamente non había ningunha opción de bibliotecas libres: ou ben estas son bibliotecas de moi baixo nivel que permiten simplemente codificar/descodificar, por exemplo, un enteiro e unha cabeceira de estrutura, ou esta biblioteca. piasn1. Vivimos nel durante varios anos e ao principio quedamos moi satisfeitos, xa que permite traballar con estruturas ASN.1 como con obxectos de alto nivel: por exemplo, un obxecto certificado X.509 descodificado permite acceder aos seus campos a través de unha interface de dicionario: cert[“tbsCertificate”] [“serialNumber”] mostraranos o número de serie deste certificado. Do mesmo xeito, pode "ensamblar" obxectos complexos traballando con eles como listas, dicionarios e, a continuación, simplemente chamar á función pyasn1.codec.der.encoder.encode e obter unha representación serializada do documento.

Non obstante, reveláronse carencias, problemas e limitacións. Houbo e, por desgraza, aínda hai erros en pyasn1: no momento de escribir este artigo, un dos tipos básicos de pyasn1 é GeneralizedTime, incorrectamente decodificado e codificado.

Nos nosos proxectos, para aforrar espazo, adoitamos almacenar só a ruta do ficheiro, o desprazamento e a lonxitude en bytes do obxecto ao que queremos facer referencia. Por exemplo, un ficheiro asinado arbitrario probablemente estará situado na estrutura ASN.1 de CMS SignedData:

  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 ficheiro orixinal asinado cunha compensación de 65 bytes, 751 bytes de lonxitude. pyasn1 non almacena esta información nos seus obxectos decodificados. Escribiuse o chamado TLVSeeker: unha pequena biblioteca que che permite decodificar etiquetas e lonxitudes de obxectos, na interface da cal mandamos "ir á seguinte etiqueta", "ir dentro da etiqueta" (ir dentro do obxecto SEQUENCE), "ir á seguinte etiqueta", "diga a súa compensación e a lonxitude do obxecto onde estamos". Este foi un paseo "manual" polos datos serializados por ASN.1 DER. Pero era imposible traballar con datos serializados por BER deste xeito, xa que, por exemplo, a cadea de bytes OCTET STRING podía codificarse en forma de varios anacos.

Outro inconveniente das nosas tarefas pyasn1 é a incapacidade de comprender a partir dos obxectos descodificados se un determinado campo estaba presente na SECUENCIA ou non. Por exemplo, se a estrutura contén un campo SEQUENCE OF Smth OPCIONAL, entón podería estar completamente ausente dos datos entrantes (OPCIONAL), ou podería estar presente, pero ser de lonxitude cero (lista baleira). En xeral, isto non se puido determinar. E isto é necesario para a verificación rigorosa da validez dos datos recibidos. Imaxina que algunha autoridade de certificación emitiría un certificado con datos que "non son totalmente" válidos desde o punto de vista dos esquemas ASN.1! Por exemplo, a autoridade de certificación "TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı" superou os límites permitidos no seu certificado raíz RFC 5280 límites na lonxitude do compoñente de materia - non se pode decodificar honestamente segundo o esquema. O códec DER require que un campo cuxo valor é igual a PREDETERMINADO non estea codificado durante a transmisión; tales documentos ocorren en vida, e a primeira versión de PyDERASN incluso permitiu deliberadamente tal comportamento non válido (desde o punto de vista DER) por mor de retrocompatibilidade.

Outra limitación é a incapacidade de descubrir facilmente en que forma (BER/DER) se codificou un determinado obxecto na estrutura. Por exemplo, o estándar CMS di que a mensaxe está codificada por BER, pero o campo signedAttrs, sobre o que se xera a sinatura criptográfica, debe estar en DER. Se decodificamos con DER, fallaremos no procesamento do propio CMS; se decodificamos con BER, non saberemos en que forma estaba o SignedAttrs. Como resultado, TLVSeeker (que non ten un análogo en pyasn1) terá que buscar a localización de cada un dos campos signedAttrs e, por separado, sacándoo da representación serializada, descodificalo con DER.

A capacidade de procesar automaticamente os campos DEFINIDO POR, que ocorren con moita frecuencia, era moi desexable para nós. Despois de decodificar a estrutura ASN.1, é posible que nos queden moitos campos CAL que deben ser procesados ​​máis de acordo co esquema seleccionado en función do IDENTIFICATIVO DE OBXECTO especificado no campo de estrutura. En código Python, isto significa escribir se e despois chamar ao decodificador para CALQUERA campo.

A aparición de PyDERASN

En Atlas, regularmente enviamos parches á parte superior cando atopamos algún problema ou melloramos os programas gratuítos que utilizamos. Enviamos melloras a pyasn1 varias veces, pero o código de pyasn1 non é o máis fácil de entender e ás veces houbo cambios na API incompatibles que nos derrotaron. Ademais, estamos afeitos a escribir probas con probas xerativas, o que non era o caso de pyasn1.

Un bo día decidín que xa estaba abondo e xa era hora de tentar escribir a miña propia biblioteca con __slot__s, compensacións e blobs ben mostrados! Simplemente crear un códec ASN.1 non sería suficiente - necesitamos transferir todos os nosos proxectos dependentes a el, e estes son centos de miles de liñas de código que están cheas de traballo coas estruturas ASN.1. É dicir, un dos requisitos para iso: facilidade de tradución do código pyasn1 actual. Despois de pasar todas as miñas vacacións, escribín esta biblioteca e transferín a ela todos os proxectos. Dado que teñen case o 100% de cobertura con probas, isto significaba que a biblioteca estaba totalmente operativa.

PyDERASN, do mesmo xeito, ten unha cobertura de proba case o 100%. Usa probas xerativas cunha gran biblioteca hipótese. Tamén se levou a cabo borrosa py-afl- Eu como en 32 máquinas nucleares. A pesar de que practicamente non nos queda código Python2, PyDERASN aínda mantén a compatibilidade con el e, por iso, ten o único seis adicción. Ademais, está probado contra ASN.1:2008 paquete de probas de conformidade.

O principio de traballar con el é semellante ao de pyasn1: traballar con obxectos de Python de alto nivel. A descrición dos esquemas ASN.1 é semellante.

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

Non obstante, PyDERASN ten unha aparencia de dixitación forte. En pyasn1, se un campo era de tipo CMSVersion(INTEGER), entón podería asignarse int ou INTEGER. PyDERASN require estrictamente que o obxecto asignado sexa exactamente CMSVersion. Ademais de escribir código Python3, tamén usamos anotacións de escritura, polo que as nosas funcións non terán argumentos escuros como def func(serial, contents), pero def func(serial: CertificateSerialNumber, contents: EncapsulatedContentInfo) e PyDERASN axuda a manter ese código.

Ao mesmo tempo, PyDERASN ten concesións moi convenientes para esta tipificación. pyasn1 non permitía que o campo SubjectKeyIdentifier().subtype(implicitTag=Tag(...)) asignase un obxecto ao SubjectKeyIdentifier() (sen a ETIQUETA IMPLÍCITA necesaria) e era necesario copiar e recrear obxectos a miúdo só por mor de as etiquetas IMPLICIT/EXPLICIT modificadas. PyDERASN observa estrictamente só o tipo base: substituirá automaticamente as etiquetas do esquema ASN.1 xa existente da estrutura. Isto simplifica moito o código da aplicación.

Se se produce un erro durante a decodificación, entón en pyasn1 non é fácil entender onde ocorreu exactamente. Por exemplo, no certificado turco xa mencionado anteriormente, recibiremos o seguinte erro: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (en 138) límites non satisfeitos: 1 ⇐ 77 ⇐ 64 Ao escribir estruturas ASN .1 a xente pode cometer erros, e isto facilita a depuración de aplicacións ou a solución de problemas cos documentos codificados da outra parte.

A primeira versión de PyDERASN non admitía a codificación BER. Apareceu moito máis tarde e aínda non admite o procesamento de UTCTime/XeneralizedTime con fusos horarios. Isto chegará no futuro, porque o proxecto está escrito principalmente no meu tempo libre.

Ademais, na primeira versión non había ningún traballo cos campos DEFINIDO POR. Uns meses despois isto xurdiu oportunidade e comezou a usarse activamente, reducindo significativamente o código da aplicación - nunha operación de decodificación foi posible obter toda a estrutura desmontada ata a profundidade. Para iso, o esquema especifica que campos "definen" que. Por exemplo, unha descrición do 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))),
    )

di que se o contentType contén un OID co valor id_signedData, entón o campo de contido (situado na mesma SEQUENCE) debe ser decodificado segundo o esquema SignedData. Por que hai tantas parénteses? Un campo pode "definir" varios campos ao mesmo tempo, como é o caso das estruturas EnvelopedData. Os campos definidos identifícanse polo chamado camiño de decodificación: especifica a localización exacta de calquera elemento en todas as estruturas.

Non sempre quere ou non sempre ten a oportunidade de engadir inmediatamente estas definicións ao diagrama. Pode haber casos específicos de aplicacións nos que os OID e as estruturas só se coñecen nun proxecto de terceiros. PyDERASN ofrece a posibilidade de establecer estas definicións correctamente no momento de decodificar a 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(),
    }),),
),)})

Aquí dicimos que en CMS SignedData para todos os certificados anexos, decodifica todas as súas extensións (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool, etc.). Indicamos a través do camiño de decodificación que elemento hai que "substituír" con defines, coma se estivese especificado no esquema.

Finalmente, PyDERASN ten a capacidade de correr desde liña de comando para decodificar ficheiros ASN.1 e ten rico bonita impresión. Podes decodificar un ASN.1 arbitrario ou podes especificar un esquema claramente definido e ver algo así:

PyDERASN: como escribín unha biblioteca ASN.1 con slots e blobs

Información mostrada: desprazamento do obxecto, lonxitude da etiqueta, lonxitude da lonxitude, lonxitude do contido, presenza de EOC (fin de octetos), atributo de codificación BER, atributo de codificación de lonxitude indefinida, lonxitude e desprazamento da etiqueta EXPLICIT (se o hai), profundidade de anidamento de o obxecto en estruturas, o valor da etiqueta IMPLICIT/EXPLICIT, o nome do obxecto segundo o esquema, o seu tipo base ASN.1, o número de secuencia dentro de SEQUENCE/SET OF, CHOICE value (se o hai), nome lexible por humanos INTEGER/ENUMERATED/BIT STRING segundo o esquema, valor de calquera tipo de base , bandeira PREDETERMINADA/OPCIONAL do esquema, un sinal de que o obxecto foi decodificado automaticamente como DEFINIDO POR e debido a que OID ocorreu, OID lexible por humanos.

O bonito sistema de impresión está especialmente deseñado para xerar unha secuencia de obxectos PP que se visualizan mediante medios separados. A captura de pantalla mostra o renderizador en texto de cores simple. Tamén hai renderizadores en formato JSON/HTML, de xeito que se pode ver con resaltado no navegador ASN.1, como en asn1js proxecto.

Outras bibliotecas

Este non era o obxectivo, pero PyDERASN resultou significativamente máis rápido que pyasn1. Por exemplo, a decodificación de ficheiros CRL de tamaños de megabyte pode levar tanto tempo que hai que pensar en formatos de almacenamento de datos intermedios (rápido) e cambiar a arquitectura da aplicación. pyasn1 descodifica CRL CACert.org no meu portátil leva máis de 20 minutos, mentres que PyDERASN leva só 28 segundos. Hai un proxecto asn1crypto, destinado a un traballo rápido con estruturas criptográficas: decodifica (completamente, non con preguiza) o mesmo CRL en 29 segundos, pero consume case o dobre de RAM cando se executa baixo Python3 (983 MiB fronte a 498), e en 3.5 veces baixo Python2 (1677). fronte a 488), mentres que pyasn1 consome ata 4.3 veces máis (2093 fronte a 488).

Non consideramos asn1crypto, o que mencionei, porque o proxecto aínda estaba na súa etapa inicial e non tiñamos oído falar del. Agora tampouco miraríamos na súa dirección, xa que inmediatamente descubrín que o mesmo GeneralizedTime non toma unha forma arbitraria, e durante a serialización elimina silenciosamente unha fracción de segundo. Isto é aceptable para traballar con certificados X.509, pero en xeral non funcionará.

Polo momento, PyDERASN é o decodificador gratuíto de Python/Go DER máis estrito que coñezo. Na biblioteca de codificación/asn1 do meu querido Go non é un control estrito OBJECT IDENTIFIER e cadeas UTCTime/GeneralizedTime. Ás veces o estricto pode interferir (principalmente debido á compatibilidade con versións anteriores con aplicacións máis antigas que ninguén arranxará), polo que PyDERASN pode pasar varias configuracións cheques debilitados.

O código do proxecto tenta ser o máis sinxelo posible. Toda a biblioteca é un ficheiro. O código está escrito con énfase na facilidade de comprensión, sen optimizacións de rendemento innecesarias e código DRY. Non admite, como xa dixen, a decodificación BER completa de cadeas UTCTime/XeneralizedTime, así como os tipos de datos REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING. En todos os demais casos, persoalmente, non vexo o sentido de usar outras bibliotecas en Python.

Como todos os meus proxectos, como PyGOST, GoGOST, NCCP, GoVPN, PyDERASN é completamente software libre, distribuído segundo os termos LGPLv3+, e está dispoñible para descarga gratuíta. Hai exemplos de uso aquí e Probas PyGOST.

Sergey Matveev, cypherpunk, membro Fundación SPO, programador Python/Go, especialista xefe FSUE "STC "Atlas".

Fonte: www.habr.com

Engadir un comentario