PyDERASN: come ho scritto una libreria ASN.1 con slots e blobs

ASN.1 questa è la norma (ISO, ITU-T, GOST) per il linguaggio che descrive informazioni strutturate e le relative regole di codifica. Per me, come programmatore, è semplicemente un altro formato di serializzazione e rappresentanza dei dati, insieme a JSON, XML, XDR e altri. È estremamente diffuso nella nostra vita quotidiana, e molti lo incontrano: nelle comunicazioni cellulari, telefoniche, VoIP (UMTS, LTE, WiMAX, SS7, H.323), nei protocolli di rete (LDAP, SNMP, Kerberos), in tutto ciò che riguarda la crittografia (X.509, CMS, standard PKCS), nelle carte bancarie e nei passaporti biometrici, e in molti altri ambiti.

In questo articolo si discute (di cui ho già: la libreria Python ASN.1 utilizzata attivamente in progetti legati alla crittografia in Atlas.

PyDERASN: come ho scritto una libreria ASN.1 con slots e blobs
In effetti, non è consigliabile utilizzare ASN.1 per compiti di crittografia: ASN.1 e i suoi codec sono complessi. Questo significa che il codice non sarà semplice, e questo rappresenta sempre un ulteriore vettore di attacco. È sufficiente guardare l'elenco delle vulnerabilità nelle librerie ASN.1. Bruce Schneier nella sua Cryptography Engineering non consiglia nemmeno di usare questo standard a causa della sua complessità: «Il codifica TLV più conosciuta è ASN.1, ma è incredibilmente complessa e noi tendiamo a evitarla». Ma, sfortunatamente, oggi abbiamo infrastrutture di chiavi pubbliche in cui vengono utilizzati attivamente certificati X.509, CRL, OCSP, TSP, protocolli CMP, CMC, messaggi CMS, e un sacco di standard PKCS. Pertanto, è necessario saper lavorare con ASN.1 se si è coinvolti in qualcosa relativo alla crittografia.

ASN.1 può essere codificato in molti modi/codec:

  • BER (Basic Encoding Rules)
  • CER (Canonical Encoding Rules)
  • DER (Distinguished Encoding Rules)
  • GSER (Generic String Encoding Rules)
  • JER (JSON Encoding Rules)
  • LWER (Light Weight Encoding Rules)
  • OER (Octet Encoding Rules)
  • PER (Packed Encoding Rules)
  • SER (Signalling specific Encoding Rules)
  • XER (XML Encoding Rules)

e altri ancora. Ma per compiti di crittografia nella pratica si usano due: BER e DER. Anche nei documenti XML firmati (XMLDSig, XAdES) ci saranno comunque oggetti ASN.1 DER codificati in Base64, così come nel protocollo orientato a JSON ACME di Let’s Encrypt. È meglio capire tutti questi codec e principi di codifica BER/CER/DER approfonditamente negli articoli e nei libri: ASN.1 in parole semplici, ASN.1 — Comunicazione tra sistemi eterogenei di Olivier Dubuisson, ASN.1 Completo di Prof. John Larmouth.

BER è un formato TLV binario orientato ai byte (ad esempio PER, popolare nelle comunicazioni cellulari — orientato ai bit). Ogni elemento è codificato come: un tag (Tag), che identifica il tipo di elemento codificato (numero intero, stringa, data, ecc.), lunghezza (Llunghezza) del contenuto e del contenuto stesso (Vvalore). BER consente facoltativamente di non specificare il valore della lunghezza, impostando un valore speciale di lunghezza indefinita e terminando il messaggio con l'etichetta End-Of-Octets. Oltre alla codifica della lunghezza, nel BER c'è molta variabilità nel modo di codificare i tipi di dato, come ad esempio:

  • INTEGER, OBJECT IDENTIFIER, BIT STRING e la lunghezza dell'elemento possono essere non normalizzate (non codificate nella forma minima);
  • BOOLEAN è vero per qualsiasi contenuto non nullo;
  • BIT STRING può contenere bit zero "di troppo";
  • BIT STRING, OCTET STRING e tutti i loro tipi di stringa derivati, compresi data/ora, possono essere suddivisi in pezzi (chunk) di lunghezza variabile, la cui lunghezza durante la (de)codifica non è nota in anticipo;
  • UTCTime/GeneralizedTime possono avere diversi modi di specificare il fuso orario e "di troppo" frazioni di secondo nulle;
  • I valori DEFAULT di SEQUENCE possono essere codificati, oppure no;
  • I valori nominati degli ultimi bit in BIT STRING possono essere facoltativamente non codificati;
  • SEQUENCE (OF)/SET (OF) possono avere un ordine arbitrario degli elementi.

A causa di quanto sopra, codificare i dati in modo che siano identici alla forma originale non è sempre possibile. Pertanto è stata ideata una sottoinsieme di regole: DER - che regola rigidamente un solo modo consentito di codifica, il che è critico per le problematiche crittografiche, dove, ad esempio, la modifica di un bit rende non valida la firma o il checksum. DER ha un notevole svantaggio: le lunghezze di tutti gli elementi devono essere note in anticipo durante la codifica, il che non consente di serializzare i dati in streaming. Il codec CER è privo di questo svantaggio, garantendo analogamente una rappresentazione inequivocabile dei dati. Sfortunatamente (o per fortuna che non abbiamo decoder ancora più complessi?), non è diventato popolare. Pertanto, nella pratica, incontriamo un uso "misto" di dati codificati in BER e DER. Poiché sia CER che DER sono sottoinsiemi di BER, qualsiasi decoder BER è in grado di elaborarli.

Problemi con pyasn1

Al lavoro scriviamo molti programmi in Python legati alla crittografia. E alcuni anni fa, le opzioni di librerie gratuite erano praticamente assenti: o si trattava di librerie a basso livello che permettevano semplicemente di codificare/decodificare, ad esempio, un numero intero e l'intestazione di una struttura, oppure era la libreria pyasn1. Abbiamo vissuto lì per diversi anni e all'inizio eravamo molto soddisfatti, poiché ci consente di lavorare con le strutture ASN.1 come se fossero oggetti di alto livello: ad esempio, l'oggetto decodificato del certificato X.509 ci consente di accedere ai suoi campi tramite un'interfaccia di dizionario: cert[«tbsCertificate»][«serialNumber»] ci mostrerà il numero di serie di questo certificato. Allo stesso modo, possiamo "costruire" oggetti complessi trattandoli come elenchi, dizionari e poi semplicemente chiamare la funzione pyasn1.codec.der.encoder.encode e ottenere una rappresentazione serializzata del documento.

Tuttavia, sono emersi difetti, problemi e limitazioni. In pyasn1 ci sono stati e, sfortunatamente, ci sono ancora errori: al momento della scrittura dell'articolo, in pyasn1 uno dei tipi di base — GeneralizedTime, non viene decodificato e codificato correttamente.

Nei nostri progetti, per risparmiare spazio, spesso memorizziamo solo il percorso al file, lo spostamento e la lunghezza in byte dell'oggetto a cui vogliamo fare riferimento. Ad esempio, un file firmato casualmente sarà certamente contenuto nella struttura ASN.1 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

                 QUI C'È IL CONTENUTO DEL FILE DA FIRMARE DELLA DIMENSIONE DI 751 byte

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 possiamo ottenere il file originale firmato con uno spostamento di 65 byte, lungo 751 byte. pyasn1 non memorizza queste informazioni nei suoi oggetti decodificati. È stata scritta una piccola libreria chiamata TLVSeeker, che consente di decodificare tag e lunghezze degli oggetti, nel cui interfaccia ordiniamo 'vai al prossimo tag', 'entra nel tag' (entriamo all'interno dell'oggetto SEQUENCE), 'vai al prossimo tag', 'riporta il tuo offset e la lunghezza dell'oggetto in cui ci troviamo'. Era un percorso 'manuale' attraverso i dati serializzati ASN.1 DER. Ma non era possibile lavorare in questo modo con i dati serializzati BER, poiché, ad esempio, una stringa di byte OCTET STRING potrebbe essere codificata in più chunk.

Un altro svantaggio per le nostre esigenze di pyasn1 è l'impossibilità di capire dagli oggetti decodificati se un campo specifico fosse presente nella SEQUENCE o meno. Ad esempio, se la struttura contiene il campo Field SEQUENCE OF Smth OPTIONAL, questo potrebbe essere completamente assente nei dati ricevuti (OPTIONAL), oppure potrebbe essere presente, ma avere una lunghezza zero (lista vuota). In generale, non era possibile stabilirlo. E questo è necessario per una rigorosa verifica della validità dei dati ricevuti. Immaginate che un qualche centro di certificazione emetta un certificato con dati 'non proprio' validi secondo gli schemi ASN.1! Ad esempio, il centro di certificazione 'TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı' nel suo certificato radice è andato oltre i limiti. RFC 5280 della lunghezza del componente subject — non è possibile decodificare onestamente secondo lo schema. Il codec DER richiede che un campo il cui valore è pari al valore DEFAULT non venga codificato durante la trasmissione — nella vita reale, tali documenti esistono, e la prima versione di PyDERASN tollerava consapevolmente questo comportamento non valido (dal punto di vista DER) per motivi di retrocompatibilità.

Un'altra limitazione è l'impossibilità di sapere facilmente in quale formato (BER/DER) è stato codificato un determinato oggetto nella struttura. Ad esempio, lo standard CMS afferma che il messaggio è codificato in BER, ma il campo signedAttrs, su cui viene generata la firma crittografica, deve essere in DER. Se decodifichiamo in DER, avremo problemi con l'elaborazione della stessa CMS; se decodifichiamo in BER, non sapremo in quale formato era signedAttrs. In definitiva, sarà necessario utilizzare TLVSeeker (il cui equivalente non esiste in pyasn1) per cercare la posizione di ciascun campo signedAttrs e decodificarlo separatamente in DER, estraendolo dalla rappresentazione serializzata.

Era molto desiderata la possibilità di elaborare automaticamente i campi DEFINED BY, che si presentano molto spesso. Dopo la decodifica della struttura ASN.1, potrebbero rimanere molti campi ANY che devono essere elaborati ulteriormente secondo uno schema scelto in base all'OBJECT IDENTIFIER specificato nel campo della struttura. In codice Python, ciò significa scrivere if e successivamente chiamare il decodificatore per il campo ANY.

L'arrivo di PyDERASN

In Atlas, siamo soliti inviare patch per risolvere problemi o perfezionare i programmi open source che usiamo. In pyasn1 abbiamo inviato miglioramenti più volte, ma il codice di pyasn1 non è dei più semplici da comprendere e, talvolta, vi sono stati cambiamenti API incompatibili che ci hanno creato problemi. Inoltre, ci eravamo abituati a scrivere test utilizzando il testing generativo, cosa che non esisteva in pyasn1.

Un giorno decisi che era ora di smettere di sopportare questa situazione e provare a scrivere la mia libreria con __slot__, offset e blob facilmente visualizzabili! Creare semplicemente un codec ASN.1 non sarebbe stato sufficiente: era necessario portare tutti i nostri progetti interdipendenti su di essa, e questo equivale a centinaia di migliaia di righe di codice piene di lavoro con strutture ASN.1. Da qui uno dei requisiti: facilità di conversione del codice attuale di pyasn1. Ho speso tutto il mio periodo di ferie scrivendo questa libreria e convertendo tutti i progetti su di essa. Poiché hanno praticamente una copertura testuale del 100%, ciò significava anche la piena funzionalità della libreria.

Anche PyDERASN ha praticamente una copertura testuale del 100%. Utilizza il testing generativo con un'ottima libreria hypothesis. Sono stati effettuati anche fuzzing py-afl-em su macchine a 32 core. Nonostante non ci sia quasi più codice Python2, PyDERASN mantiene comunque la compatibilità con esso e per questo ha una sola six dipendenza. Inoltre, è stato testato rispetto alla suite di test di conformità ASN.1:2008.

Il principio di funzionamento è simile a pyasn1: si lavora con oggetti Python ad alto livello. La descrizione degli schemi ASN.1 è analoga.

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

Tuttavia, PyDERASN ha una somiglianza con la tipizzazione rigorosa. In pyasn1, se un campo aveva il tipo CMSVersion(INTEGER), si poteva assegnare un int o INTEGER. PyDERASN richiede rigorosamente che l'oggetto assegnato sia proprio CMSVersion. Inoltre, poiché scriviamo codice Python3, utilizziamo anche annotazioni di tipo, quindi nelle nostre funzioni avremo argomenti chiari del tipo def func(serial, contents) e non stravaganti, ma def func(serial: CertificateSerialNumber, contents: EncapsulatedContentInfo), e PyDERASN aiuta a mantenere questo tipo di codice.

In PyDERASN, ci sono comodità molto utili per questo stesso tipo. pyasn1 non consentiva di assegnare a SubjectKeyIdentifier().subtype(implicitTag=Tag(…)) un oggetto SubjectKeyIdentifier() (senza il necessario IMPLICIT TAG) e spesso si dovevano copiare e ricreare oggetti solo a causa di tag IMPLICIT/EXPLICIT modificati. PyDERASN rispetta rigidamente solo il tipo base: i tag vengono automaticamente inseriti dalla schema ASN.1 esistente. Questo semplifica notevolmente il codice delle applicazioni.

Se si verifica un errore durante la decodifica, in pyasn1 non è facile capire dove sia successo. Ad esempio, nel certificato turco menzionato in precedenza, otteniamo un errore del genere: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (at 138) bounds insoddisfatti: 1 ⇐ 77 ⇐ 64. Nella scrittura delle strutture ASN.1, le persone possono commettere errori, e questo aiuta a debug più facilmente le applicazioni o a scoprire problemi nei documenti codificati della parte opposta.

Nella prima versione, PyDERASN non supportava la codifica BER. È stata introdotta molto più tardi e tuttora non è supportata la gestione di UTCTime/GeneralizedTime con i fusi orari. Questo arriverà in futuro, poiché il progetto viene scritto principalmente nel tempo libero.

Nella prima versione non c'era alcun lavoro con i campi DEFINED BY. Dopo alcuni mesi, questa funzionalità è stata implementata ed è stata utilizzata attivamente, riducendo notevolmente il codice delle applicazioni: con un'unica operazione di decodifica si poteva ottenere l'intera struttura analizzata fino in fondo. Per fare ciò, nello schema vengono specificati quali campi "definiscono" cosa. Ad esempio, la descrizione dello schema 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))),
    )

indica che se contentType contiene OID con il valore id_signedData, allora il campo content (che si trova in questo stesso SEQUENCE) deve essere decodificato secondo lo schema SignedData. Perché ci sono così tante parentesi? Un campo può "definire" più campi contemporaneamente, come avviene nelle strutture EnvelopedData. I campi definiti sono identificati dal cosiddetto decode path — esso stabilisce la posizione esatta di qualsiasi elemento in tutte le strutture.

Non sempre si desidera o si ha la possibilità di aggiungere immediatamente questi defines nello schema. Possono esserci casi specifici dell'applicazione quando OID e strutture sono conosciuti solo in un progetto esterno. PyDERASN offre la possibilità di definire questi defines proprio al momento della decodifica della struttura:

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

Qui diciamo che nel CMS SignedData per tutti i certificati allegati, decodificare tutte le loro estensioni (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool, ecc.). Indichiamo attraverso il decode path a quale elemento devono essere "sovrapposti" i defines, come se fossero stati specificati nello schema.

Infine, PyDERASN ha la possibilità di lavorare da riga di comando per la decodifica di file ASN.1 e ha un ricco pretty printing. È possibile decodificare qualsiasi ASN.1, oppure specificare uno schema ben definito e vedere qualcosa di simile:

PyDERASN: come ho scritto una libreria ASN.1 con slots e blobs

Informazioni visualizzate: offset dell'oggetto, lunghezza del tag, lunghezza della lunghezza, lunghezza del contenuto, presenza di EOC (end-of-octets), indicatore della codifica BER, indicatore della codifica a lunghezza indefinita, lunghezza e offset del tag EXPLICIT (se presente), profondità di nidificazione dell'oggetto nelle strutture, valore del tag IMPLICIT/EXPLICIT, nome dell'oggetto secondo lo schema, il suo tipo ASN.1 di base, numero di sequenza all'interno di SEQUENCE/SET OF, valore CHOICE (se presente), nome leggibile dell'INTEGER/ENUMERATED/BIT STRING secondo lo schema, valore di qualsiasi tipo di base, flag DEFAULT/OPTIONAL dello schema, indicatore che l'oggetto è stato automaticamente decodificato come DEFINED BY e a causa di quale OID è avvenuto, OID leggibile.

Il sistema di pretty printing è progettato in modo tale da generare una sequenza di oggetti PP, che vengono visualizzati separatamente tramite strumenti. Nello screenshot è mostrato il renderer in semplice testo colorato. Esistono anche renderer in formato JSON/HTML, in modo da poterlo vedere con evidenziazione nel browser ASN.1 come in asn1js progetto.

Altre librerie

Non era l'obiettivo, ma PyDERASN è risultato significativamente più veloce rispetto a pyasn1. Ad esempio, la decodifica di file CRL di dimensioni megabyte può richiedere un tempo così lungo che bisognerà considerare formati intermedi di archiviazione dati (rapidi) e modificare l'architettura delle applicazioni. pyasn1 decodifica i file CRL CACert.org sul mio laptop in oltre 20 minuti, mentre PyDERASN in soli 28 secondi! Esiste un progetto asn1crypto, focalizzato su un uso rapido di strutture crittografiche: decodifica (completamente, non pigro) lo stesso CRL in 29 secondi, ma consuma quasi il doppio della memoria RAM all'avvio sotto Python3 (983 MiB contro 498), e 3,5 volte di più sotto Python2 (1677 contro 488), mentre pyasn1 consuma addirittura 4,3 volte di più (2093 contro 488).

asn1crypto, che ho menzionato, non lo abbiamo preso in considerazione, perché il progetto era ancora in fase di avvio e non ne avevamo sentito parlare. Anche ora non lo esamineremmo, poiché ho scoperto subito che lo stesso GeneralizedTime non accetta un formato arbitrario e durante la serializzazione rimuove silenziosamente le frazioni di secondo. Questo è accettabile per lavorare con certificati X.509, ma in generale non andrebbe bene.

Attualmente, PyDERASN è il più rigoroso tra i decoder DER gratuiti di Python/Go a me noti. Nella libreria encoding/asn1 del mio amato Go non c'è un controllo rigoroso IDENTIFICATORE DELL'OGGETTO e stringhe UTCTime/GeneralizedTime. A volte la rigidità può essere un ostacolo (specialmente a causa della retrocompatibilità con le vecchie applicazioni che nessuno riparerà), quindi in PyDERASN durante la decodifica si possono passare diverse impostazioni che allentano i controlli.

Il codice del progetto cerca di essere il più semplice possibile. L'intera libreria è un file unico. Il codice è scritto con un focus sulla facilità di comprensione, senza eccessive ottimizzazioni delle prestazioni e codice DRY. Come già detto, non supporta la decodifica completa di stringhe UTCTime/GeneralizedTime, né dei tipi di dati REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING. In tutti gli altri casi, personalmente non vedo il senso di usare altre librerie in Python.

Come tutti i miei progetti, tipo PyGOST, GoGOST, NNCP, GoVPN, PyDERASN è completamente software libero, distribuito secondo i termini della LGPLv3+, ed è disponibile per il download gratuito. Ci sono esempi di utilizzo nei qui e in test di PyGOST.

Sergey Matveev, cipherpunk, membro Fondo Soft Open, sviluppatore Python/Go, specialista principale FGUP "NTC Atlas".

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster