è uno standard (ISO, ITU-T, GOST) per il linguaggio che descrive informazioni strutturate e le regole per la codifica di tali informazioni. Per me, come programmatore, è semplicemente un altro formato di serializzazione e rappresentazione dei dati, come JSON, XML, XDR e altri. È molto diffuso nella nostra vita quotidiana e molte persone lo incontrano: nella comunicazione cellulare, telefonica, 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 contesti.
In questo articolo si discute : la libreria Python ASN.1, ampiamente utilizzata in progetti legati alla crittografia in .

In effetti, non è consigliabile utilizzare ASN.1 per compiti crittografici: ASN.1 e i suoi codec sono complessi. Questo significa che il codice non sarà semplice, e ciò rappresenta sempre un ulteriore vettore di attacco. È sufficiente guardare delle vulnerabilità nelle librerie ASN.1. Bruce Schneier nel suo non consiglia nemmeno di utilizzare questo standard a causa della sua complessità: «Il TLV encoding più conosciuto è ASN.1, ma è incredibilmente complesso e ne stiamo alla larga». Purtroppo, oggi abbiamo in cui viene utilizzato attivamente , CRL, OCSP, TSP, protocolli CMP, , messaggi , e una miriade di standard . Pertanto, è necessario saper lavorare con ASN.1 se ci si occupa di qualcosa legato alla crittografia.
ASN.1 può essere codificato in vari modi/codecs:
- (Regole di Codifica di Base)
- (Regole di Codifica Canonica)
- (Regole di Codifica Distinte)
- (Regole di Codifica Generica per Stringhe)
- (Regole di Codifica JSON)
- LWER (Regole di Codifica Leggera)
- (Regole di Codifica Octet)
- (Regole di Codifica Compressa)
- SER (Regole di Codifica Specifiche per Segnalazione)
- (Regole di Codifica XML)
e molti altri. Ma nella pratica crittografica, vengono utilizzati principalmente due: BER e DER. Anche nei documenti XML firmati (, ) ci saranno comunque oggetti ASN.1 DER codificati in Base64, così come nel protocollo orientato a JSON di Let’s Encrypt. È meglio approfondire tutti questi codec e principi di codifica BER/CER/DER nei seguenti articoli e libri: , , .
BER è un formato TLV binario orientato ai byte (ad esempio PER, popolare nella telefonia mobile — orientato ai bit). Ogni elemento è codificato come: tag (Tag), identificando il tipo di elemento codificato (numero intero, stringa, data, ecc.), lunghezza (Length) del contenuto e il contenuto stesso (VIl formato BER consente di non specificare la lunghezza, usando un valore di lunghezza indefinita e terminando il messaggio con il tag End-Of-Octets. Oltre alla codifica della lunghezza, nel BER c'è molta variabilità nel modo in cui vengono codificati i tipi di dati, come ad esempio:
- INTEGER, OBJECT IDENTIFIER, BIT STRING e la lunghezza dell'elemento possono non essere normalizzati (non codificati nella forma minima);
- BOOLEAN è vero per qualsiasi contenuto non nullo;
- BIT STRING può contenere bit zero 'in più';
- BIT STRING, OCTET STRING e tutti i loro tipi stringa derivati, inclusi data/ora, possono essere suddivisi in chunk di lunghezza variabile, la cui lunghezza non è nota durante la (de)codifica;
- UTCTime/GeneralizedTime possono avere diversi modi di specificare il fuso orario e 'in più' frazioni di secondo zero;
- I valori DEFAULT in SEQUENCE possono essere codificati o meno;
- 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.
Из-за всего вышеназванного, закодировать данные так, чтобы они были идентичны оригинальной форме — не всегда возможно. Поэтому было придумано подмножество правил: DER — жёстко регламентирующий только один допустимый способ кодирования, что критично для криптографических задач, где, например, изменение одного бита сделает подпись или контрольную сумму недействительной. DER имеет существенный недостаток: длины всех элементов должны быть заранее известны во время кодирования, что не позволяет потоково сериализовать данные. CER кодек лишён этого недостатка, аналогично гарантируя однозначное представление данных. К сожалению (или счастью что не имеем ещё более сложные декодеры?), он не стал популярен. Поэтому на практике мы встречаем «смешанное» использование BER и DER закодированных данных. Так как и CER и DER являются подмножеством BER, то любой BER-декодер способен их обработать.
Проблемы с pyasn1
Nel nostro lavoro scriviamo molti programmi in Python legati alla crittografia. Alcuni anni fa le librerie gratuite erano praticamente inesistenti: c'erano solo librerie a basso livello che consentivano di codificare/decodificare, ad esempio, un numero intero e l'intestazione di una struttura, oppure questa libreria . Abbiamo utilizzato questa libreria per diversi anni e inizialmente eravamo molto soddisfatti, poiché consente di lavorare con strutture ASN.1 come oggetti ad alto livello: ad esempio, l'oggetto decodificato del certificato X.509 permette di accedere ai suoi campi tramite un'interfaccia a dizionario: cert[«tbsCertificate»][«serialNumber»] ci mostrerà il numero di serie di questo certificato. Allo stesso modo, è possibile "costruire" oggetti complessi trattandoli come liste e dizionari, e poi semplicemente chiamare la funzione pyasn1.codec.der.encoder.encode per ottenere la rappresentazione serializzata del documento.
Tuttavia, sono emersi difetti, problemi e limitazioni. In pyasn1 ci sono stati e, sfortunatamente, ci sono ancora bug: al momento della scrittura di questo articolo, in pyasn1 uno dei tipi di base — GeneralizedTime, decodificato e codificato in modo corretto.
Nei nostri progetti, per risparmiare spazio, spesso memorizziamo solo il percorso al file, l'offset e la lunghezza in byte dell'oggetto a cui vogliamo riferirci. Ad esempio, un file firmato arbitrario sarà sicuramente contenuto nella struttura 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] . . DEFINITO DA 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 IL CONTENUTO DEL FILE DA FIRMARE DI DIMENSIONE 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 estrarre il file originale firmato con uno spostamento di 65 byte, lungo 751 byte. pyasn1 non memorizza queste informazioni nei suoi oggetti decodificati. È stata scritta quella che viene chiamata TLVSeeker — una piccola libreria che consente di decodificare i tag e le lunghezze degli oggetti, nell'interfaccia della quale abbiamo dato i comandi "vai al tag successivo", "entra nel tag" (passando dentro l'oggetto SEQUENCE), "vai al tag successivo", "riporta il tuo offset e la lunghezza dell'oggetto in cui ci troviamo". Questo è stato un percorso "manuale" attraverso i dati ASN.1 serializzati in DER. Ma non era possibile lavorare in questo modo con i dati serializzati in BER, poiché, ad esempio, una stringa di byte OCTET STRING poteva essere codificata in più chunk.
Un altro svantaggio per le nostre esigenze di pyasn1 è l'impossibilità di comprendere dai dati decodificati se un determinato campo fosse presente nella SEQUENCE o meno. Ad esempio, se la struttura contiene un campo Field SEQUENCE OF Smth OPTIONAL, esso potrebbe non essere presente nei dati ricevuti (OPTIONAL), oppure potrebbe essere presente ma avere una lunghezza zero (lista vuota). In generale, non era possibile determinarlo. Questo è necessario per una rigorosa validazione dei dati ricevuti. Immagina che un'autorità di certificazione rilasci un certificato con dati 'non completamente' validi secondo gli schemi ASN.1! Ad esempio, l'autorità di certificazione 'TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı' ha oltrepassato i limiti accettabili nel suo certificato radice. limiti di lunghezza del componente subject — non può essere decodificato correttamente secondo lo schema. Il codec DER richiede che un campo il cui valore è pari al valore DEFAULT non venga codificato durante la trasmissione — nella realtà, è possibile imbattersi in documenti del genere, e la prima versione di PyDERASN permetteva anche consapevolmente tale comportamento non valido (secondo le specifiche DER) per mantenere la retrocompatibilità.
Un'altra limitazione è la difficoltà di determinare in quale formato (BER/DER) è stato codificato un certo 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 con DER, ci fermeremo durante l'elaborazione del CMS stesso; se decodifichiamo con BER, non sapremo in quale formato si trovava signedAttrs. In definitiva, sarà necessario utilizzare TLVSeeker (il cui analogo non esiste in pyasn1) per cercare la posizione di ciascun campo signedAttrs e decodificarlo separatamente, estraendolo dalla rappresentazione serializzata e utilizzando DER.
Desideravamo molto avere la possibilità di elaborazione automatica dei campi DEFINED BY, che si presentano molto frequentemente. Dopo la decodifica della struttura ASN.1, possono rimanere molti campi ANY, che devono essere ulteriormente elaborati secondo uno schema scelto in base all'OBJECT IDENTIFIER specificato nel campo della struttura. In codice Python, ciò significa scrivere un if e successivamente chiamare il decoder per il campo ANY.
L'emergere di PyDERASN
In Atlas, we regularly submit patches upstream when we find issues or enhance the free software we use. In pyasn1, we sent several modifications, but the code is not the simplest to understand and sometimes experienced incompatible API changes that hindered our progress. Additionally, we are used to writing tests with generative testing, which was absent in pyasn1.
One fine day, I decided that I could no longer tolerate this and it was time to try writing my own library with __slots__, offsets, and beautifully displayed blobs! Simply creating an ASN.1 codec wouldn't be enough — we need to port all of our interdependent projects to it, which amounts to hundreds of thousands of lines of code full of ASN.1 structure work. One of its requirements was the ease of converting the existing pyasn1 code. Spending my entire vacation, I wrote this library and ported all projects to it. Since they have nearly 100% test coverage, this meant the library was fully functional.
PyDERASN, analogamente, ha una copertura praticamente del 100% nei test. Utilizza il testing generativo con una libreria straordinaria. . Sono stati effettuati anche dei -su macchine a 32 core. Nonostante ci sia praticamente poco codice Python2 rimasto, PyDERASN mantiene la compatibilità con esso e per questo ha una sola dipendenza. Inoltre, è stato testato rispetto alla .
Il principio di funzionamento è simile a pyasn1 — lavorando 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 certa forma di tipizzazione rigorosa. In pyasn1, se un campo aveva il tipo CMSVersion(INTEGER), gli si poteva assegnare un int o INTEGER. PyDERASN richiede rigorosamente che l'oggetto assegnato sia proprio CMSVersion. Oltre al fatto che scriviamo codice in Python3, utilizziamo anche , quindi nelle nostre funzioni ci saranno argomenti chiari come def func(serial: CertificateSerialNumber, contents: EncapsulatedContentInfo), e PyDERASN aiuta a mantenere questo codice in ordine.
In PyDERASN ci sono anche molto comode concessioni a questo tipo di tipizzazione. pyasn1 non consentiva di assegnare un oggetto SubjectKeyIdentifier() al campo in SubjectKeyIdentifier().subtype(implicitTag=Tag(…)) (senza il necessario IMPLICIT TAG) e spesso si dovevano copiare e ricreare oggetti solo a causa di tag IMPLICIT/EXPLICIT modificati. PyDERASN mantiene rigorosamente solo il tipo base — i tag vengono automaticamente inseriti dalla già esistente struttura ASN.1. Questo semplifica notevolmente il codice delle applicazioni.
Quando si verifica un errore durante la decodifica, in pyasn1 non è semplice capire dove si è verificato esattamente. Ad esempio, nel certificato turco già menzionato, otterremo un errore del tipo: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (at 138) bounds non soddisfatti: 1 ⇐ 77 ⇐ 64. Quando si scrivono strutture ASN.1, gli esseri umani possono sbagliare, e questo aiuta a debugare più facilmente le applicazioni o a scoprire problemi nei documenti codificati della parte avversaria.
Nella prima versione di PyDERASN non era supportata la codifica BER. Questa funzionalità è stata introdotta molto tempo dopo e ancora non è supportata la gestione di UTCTime/GeneralizedTime con i fusi orari. Questo arriverà in futuro, poiché il progetto è sviluppato principalmente nel tempo libero.
Nella prima versione non era prevista la gestione dei campi DEFINED BY. Dopo alcuni mesi, questa e ha iniziato a essere utilizzata attivamente, riducendo notevolmente il codice delle applicazioni: con un'unica operazione di decodifica era possibile ottenere l'intera struttura analizzata in profondità. A tal fine, nello schema vengono definiti quali campi cosa "definiscono". 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))),
)
riguarda il fatto che se contentType contiene un OID con il valore id_signedData, allora il campo content (che si trova nella stessa SEQUENCE) deve essere decodificato secondo lo schema SignedData. Perché ci sono così tante parentesi? Il campo può "definire" più campi contemporaneamente, come avviene nelle strutture EnvelopedData. I campi definiti vengono identificati tramite quello che si chiama decode path — che determina la posizione esatta di qualsiasi elemento in tutte le strutture.
Non sempre si desidera o si ha la possibilità di inserire immediatamente questi defines nello schema. Potrebbero esserci casi specifici all'applicazione in cui gli OID e le strutture sono noti solo in un progetto esterno. PyDERASN fornisce la possibilità di specificare questi defines direttamente 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 affermiamo che nel CMS SignedData è necessario decodificare tutte le estensioni per i certificati allegati (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool, ecc.). Indichiamo tramite il percorso di decodifica quale elemento deve "sostituire" le definizioni, come se fosse stato definito nello schema.
Infine, PyDERASN ha la possibilità di lavorare da per decodificare i file ASN.1 e offre una ricca . È possibile decodificare un ASN.1 arbitrario o specificare uno schema definito per vedere qualcosa di simile:

Informazioni mostrate: offset dell'oggetto, lunghezza del tag, lunghezza della lunghezza, lunghezza del contenuto, presenza di EOC (end-of-octets), indicatore di codifica BER, indicatore di codifica a lunghezza indefinita, lunghezza e offset del tag EXPLICIT (se presente), profondità di annidamento dell'oggetto nelle strutture, valore del tag IMPLICIT/EXPLICIT, nome dell'oggetto secondo lo schema, il suo tipo ASN.1 di base, numero di ordine all'interno di SEQUENCE/SET OF, valore di CHOICE (se presente), nome leggibile dell'INTEGER/ENUMERATED/BIT STRING secondo lo schema, valore di qualsiasi tipo base, flag DEFAULT/OPTIONAL dallo 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 per generare una sequenza di oggetti PP, che vengono successivamente visualizzati tramite strumenti separati. Nello screenshot è mostrato un renderer in testo semplice colorato. Sono disponibili anche renderer in formato JSON/HTML, in modo da poterlo visualizzare con evidenziazione in un browser ASN.1 come in progetto.
Altre librerie
Non era l'obiettivo, ma PyDERASN risulta significativamente rispetto a pyasn1. Ad esempio, la decodifica di file CRL di grandi dimensioni può richiedere un tempo così lungo da dover considerare formati intermedi di archiviazione dei dati (veloci) e cambiare l'architettura delle applicazioni. pyasn1 decodifica CRL sul mio laptop in oltre 20 minuti, mentre PyDERASN impiega solo 28 secondi! C'è un progetto , mirato a un rapido lavoro con strutture crittografiche: decodifica (completamente, non pigra) questo stesso CRL in 29 secondi, ma consuma quasi il doppio della memoria RAM quando viene eseguito 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 considerato perché il progetto era ancora in fase embrionale e non ne avevamo sentito parlare. Anche ora non ci faremmo caso, poiché ho subito notato che lo stesso GeneralizedTime non accetta formati arbitrari e, durante la serializzazione, elimina silenziosamente i millisecondi. Questo è accettabile per lavorare con i certificati X.509, ma in generale non è adatto.
Al momento, PyDERASN è il più rigoroso tra i decodificatori DER gratuiti di Python/Go che conosco. Nella libreria encoding/asn1 del mio amato Go OBJECT IDENTIFIER e stringhe UTCTime/GeneralizedTime. A volte la rigidità può ostacolare (soprattutto a causa della retrocompatibilità con vecchie applicazioni che nessuno andrà a correggere), quindi in PyDERASN durante la decodifica è possibile passare che allentano i controlli.
Il codice del progetto cerca di essere il più semplice possibile. L'intera libreria è un unico file. Il codice è scritto con un focus sulla facilità di comprensione, senza eccessive ottimizzazioni delle prestazioni e evitando il codice DRY. Come già detto, non c'è supporto per la piena decodifica BER delle stringhe UTCTime/GeneralizedTime, né per i tipi di dati REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING. In tutti gli altri casi, personalmente non vedo motivo di utilizzare altre librerie in Python.
Come tutti i miei progetti, tipo , , , , PyDERASN è completamente , distribuito sotto i termini , ed è disponibile per il download gratuito. Esempi di utilizzo sono e nel .
, , membro , sviluppatore Python/Go, esperto principale .
Fonte: habr.com
