PyDERASN: jak napisałem bibliotekę ASN.1 z slots i blobs

ASN.1 to standard (ISO, ITU-T, ANSI) for describing structured information, as well as rules for encoding this information. For me as a programmer, it is just another data serialization and representation format, alongside JSON, XML, XDR, and others. It is extremely common in our everyday lives, encountered in mobile, telephone, VoIP communications (UMTS, LTE, WiMAX, SS7, H.323), in network protocols (LDAP, SNMP, Kerberos), in everything related to cryptography (X.509, CMS, PKCS standards), in bank cards and biometric passports, and many other areas.

This article discusses PyDERASN: Python ASN.1 library actively used in cryptography-related projects in Atlas.

PyDERASN: jak napisałem bibliotekę ASN.1 z slots i blobs
In general, recommending ASN.1 for cryptographic tasks is not advisable: ASN.1 and its codecs are complex. This means the code will not be simple, which always presents an additional attack vector. Just look at the list of vulnerabilities in ASN.1 libraries. Bruce Schneier in his Cryptography Engineering also advises against using this standard due to its complexity: "The best-known TLV encoding is ASN.1, but it is incredibly complex and we shy away from it." Unfortunately, today we have public key infrastructures in which X.509 certificates, CRL, OCSP, TSP, CMP protocols, CMC, messages CMS, and a mass of standards PKCS. Therefore, one must know how to work with ASN.1 if you are involved in anything related to cryptography.

ASN.1 can be encoded in many ways/codecs:

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

and several others. However, in practical cryptographic tasks, two are typically used: BER and DER. Even in signed XML documents (XMLDSig, XAdES) there will still be Base64-encoded ASN.1 DER objects, just as in the JSON-oriented protocol ACME from Let’s Encrypt. It's better to understand all these codecs and encoding principles BER/CER/DER in articles and books: ASN.1 in simple words, ASN.1 — Communication between heterogeneous systems by Olivier Dubuisson, ASN.1 Complete by Prof John Larmouth.

BER is a binary byte-oriented (for example, PER, popular in mobile communications — bit-oriented) TLV format. Each element is encoded as: a tag (Tag), identifying the type of the encoded element (integer, string, date, etc.), its length (Ldługości) zawartości i samej zawartości (Vwartości). BER opcjonalnie pozwala na pominięcie wartości długości, ustalając specjalną wartość o nieskończonej długości i kończąc wiadomość etykietą End-Of-Octets. Oprócz kodowania długości, w BER jest wiele wariantów w sposobie kodowania typów danych, takich jak:

  • INTEGER, OBJECT IDENTIFIER, BIT STRING oraz długość elementu mogą być nienormowane (niezakodowane w minimalnej formie);
  • BOOLEAN jest prawdziwy przy jakiejkolwiek niezerowej zawartości;
  • BIT STRING może zawierać „zbędne” zera;
  • BIT STRING, OCTET STRING i wszystkie ich pochodne typy łańcuchowe, w tym data/czas, mogą być podzielone na kawałki (chunk) zmiennej długości, dla których długość podczas (de)kodowania nie jest wcześniej znana;
  • UTCTime/GeneralizedTime mogą mieć różne sposoby określania przesunięcia strefy czasowej oraz „zbędne” zera ułamków sekund;
  • Wartości DEFAULT SEQUENCE można kodować, a można i nie;
  • Nazwane wartości ostatnich bitów w BIT STRING można opcjonalnie nie kodować;
  • SEQUENCE (OF)/SET (OF) mogą mieć dowolną kolejność elementów.

Z powodu powyższych rozważań, zakodowanie danych tak, aby były identyczne z oryginalną formą — nie zawsze jest możliwe. Dlatego wymyślono podzbiór zasad: DER — ściśle regulujący tylko jeden dozwolony sposób kodowania, co jest krytyczne dla zadań kryptograficznych, gdzie, na przykład, zmiana jednego bitu uczyni podpis lub sumę kontrolną nieważną. DER ma istotną wadę: długości wszystkich elementów muszą być znane z góry podczas kodowania, co uniemożliwia strumieniowe serializowanie danych. Codec CER nie ma tej wady, podobnie zapewniając jednoznaczne przedstawienie danych. Niestety (lub na szczęście, że nie mamy jeszcze bardziej skomplikowanych dekoderów?), nie zyskał popularności. Dlatego w praktyce spotykamy „mieszane” użycie danych zakodowanych w BER i DER. Ponieważ zarówno CER, jak i DER są podzbiorami BER, to każdy dekoder BER jest zdolny je obsługiwać.

Problemy z pyasn1

W pracy piszemy wiele programów w Pythonie związanych z kryptografią. I kilka lat temu wybór wolnych bibliotek był praktycznie żaden: albo to bardzo niskopoziomowe biblioteki, pozwalające po prostu kodować/dekodować, na przykład, liczbę całkowitą i nagłówek struktury, albo to była biblioteka pyasn1. Mieszkaliśmy na niej przez kilka lat i na początku byliśmy bardzo zadowoleni, ponieważ pozwalała na pracę z strukturami ASN.1 jako z obiektami na wysokim poziomie: na przykład zdekodowany obiekt certyfikatu X.509 pozwala na dostęp do jego pól przez interfejs słownikowy: cert[„tbsCertificate”][„serialNumber”] pokaże nam numer seryjny tego certyfikatu. Podobnie można „zbierać” złożone obiekty, pracując z nimi jak z listami, słownikami, a potem po prostu wywołać funkcję pyasn1.codec.der.encoder.encode i uzyskać zserializowaną reprezentację dokumentu.

Jednak pojawiały się wady, problemy i ograniczenia. W pyasn1 były i, niestety, wciąż pozostają błędy: w momencie pisania artykułu, w pyasn1 jeden z podstawowych typów — GeneralizedTime, jest niepoprawnie dekodowany i kodowany.

W naszych projektach, aby zaoszczędzić miejsce, często przechowujemy tylko ścieżkę do pliku, przesunięcie i długość w bajtach obiektu, do którego chcemy się odwołać. Na przykład dowolny podpisany plik na pewno będzie znajdował się w strukturze CMS SignedData ASN.1:

  0     [1,3,1018]  ContentInfo SEKWENCJA
  4     [1,1,   9]   . typeContent: ContentType IDENTYFIKATOR OBIEKTU 1.2.840.113549.1.7.2 (id_signedData)
 19-4   [0,0,1003]   . content: [0] EKSPLICYTNY [UNIV 16] DOWOLNY
 19     [1,3, 999]   . . ZDEFINIOWANE PRZEZ id_signedData: SignedData SEKWENCJA
 23     [1,1,   1]   . . . wersja: CMSVersion LICZBA CAŁKOWITA v3 (03)
 26     [1,1,  19]   . . . identyfikatoryAlgorytmówHaszujących: DigestAlgorithmIdentifiers ZESTAW
                           [...]
 47     [1,3, 769]   . . . encapContentInfo: EncapsulatedContentInfo SEKWENCJA
 51     [1,1,   8]   . . . . eContentType: ContentType IDENTYFIKATOR OBIEKTU 1.3.6.1.5.5.7.12.2 (id_cct_PKIData)
 65-4   [1,3, 751]   . . . . eContent: [0] EKSPLICYTNY CIĄG OCTETÓW 751 bajtów OPCJONALNY

                 TUTAJ ZAWARTOŚĆ PODPISYWANego PLIKI o ROZMIARZE 751 bajtów

820     [1,2, 199]   . . . signerInfos: SignerInfos ZESTAW
823     [1,2, 196]   . . . . 0: SignerInfo SEKWENCJA
826     [1,1,   1]   . . . . . wersja: CMSVersion LICZBA CAŁKOWITA v3 (03)
829     [0,0,  22]   . . . . . sid: SignerIdentifier WYBÓR subjectKeyIdentifier
                               [...]
956     [1,1,  64]   . . . . . podpis: SignatureValue CIĄG OCTETÓW 64 bajty
                     . . . . . . 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

Możemy pozyskać oryginalny plik podpisany z przesunięciem 65 bajtów, o długości 751 bajta. Pyasn1 nie przechowuje tych informacji w swoich zdekodowanych obiektach. Została napisana tzw. TLVSeeker — niewielka biblioteka umożliwiająca dekodowanie tagów i długości obiektów, w której interfejsie wydawaliśmy polecenia »przejdź do następnego tagu«, »wejdź w tag« (wchodzimy do obiektu SEQUENCE), »przejdź do następnego tagu«, »podaj swój offset i długość obiektu, w którym się znajdujemy«. To było »ręczne« przechodzenie po danych zserializowanych ASN.1 DER. Jednak takie podejście nie było możliwe dla danych zserializowanych BER, ponieważ na przykład ciąg bajtów OCTET STRING mógł być zakodowany jako kilka kawałków.

Innym problemem dla naszych potrzeb pyasn1 jest niemożność ustalenia na podstawie zdekodowanych obiektów, czy dane pole było obecne w SEQUENCE, czy nie. Na przykład, jeśli struktura zawiera pole Field SEQUENCE OF Smth OPTIONAL, to mogło ono całkowicie brakować w przesyłanych danych (OPTIONAL), a mogło być obecne, ale o zerowej długości (pusty zbiór). W ogólnym przypadku tego nie można było ustalić. A to jest niezbędne do rygorystycznego sprawdzania poprawności otrzymywanych danych. Wyobraźcie sobie, że jakiś urząd certyfikacji wydałby certyfikat z »nie do końca« poprawnymi danymi według schematu ASN.1! Przykładowo, urząd certyfikacji »TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı« w swoim certyfikacie podstawowym przekroczył dopuszczalne RFC 5280 granice długości komponentu subject — nie można go prawidłowo zdekodować według schematu. Dekoder DER wymaga, aby pole, którego wartość jest równa wartości DEFAULT, nie było kodowane podczas transmisji — w rzeczywistości takie dokumenty się zdarzają, a pierwsza wersja PyDERASN świadomie dopuszczała takie niepoprawne (z perspektywy DER) zachowanie dla zapewnienia zgodności wstecznej.

Kolejnym ograniczeniem jest niemożność łatwego ustalenia, w jakiej postaci (BER/DER) zakodowany jest dany obiekt w strukturze. Przykładowo, standard CMS mówi, że wiadomość jest kodowana w BER, ale pole signedAttrs, na którym formowana jest podpis kryptograficzny, musi być w DER. Jeśli dekodujemy w DER, napotkamy problem podczas przetwarzania samego CMS, a jeśli dekodujemy w BER, nie dowiemy się, w jakiej postaci znajduje się signedAttrs. W rezultacie będziemy musieli użyć TLVSeeker (którego odpowiednika nie ma w pyasn1), aby zlokalizować każde z pól signedAttrs i osobno je dekodować w DER z zserializowanego przedstawienia.

Dla nas bardzo pożądana była możliwość automatycznego przetwarzania pól DEFINED BY, które występują bardzo często. Po dekodowaniu struktury ASN.1 może pozostać wiele pól ANY, które muszą być dalej przetwarzane zgodnie z schematem wybranym na podstawie OBJECT IDENTIFIER określonego w polu struktury. W kodzie Pythona oznacza to napisanie instrukcji if i dalsze wywołanie dekodera dla pola ANY.

Pojawienie się PyDERASN

W Atlasie regularnie, po znalezieniu problemów lub rozwijaniu wykorzystywanych programów open source, wysyłamy poprawki w górę. W pyasn1 wysyłaliśmy kilka razy poprawki, ale kod pyasn1 nie jest najprostszy do zrozumienia i czasami zachodziły w nim niekompatybilne zmiany API, które nas ograniczały. Ponadto przyzwyczailiśmy się do pisania testów z generatywnym testowaniem, czego brakowało w pyasn1.

Pewnego pięknego dnia postanowiłem, że dość tego znoszenia i czas spróbować napisać własną bibliotekę z __slot__-ami, offset-ami i doskonale wyświetlanymi blob-ami! Stworzenie kodera ASN.1 byłoby niewystarczające — trzeba przetłumaczyć wszystkie nasze wzajemnie zależne projekty na nią, a to setki tysięcy linii kodu, w których jest sporo pracy z strukturami ASN.1. Innymi słowy, jednym z wymagań dla niej była łatwość przeniesienia obecnego kodu pyasn1. Spędziłem całe wakacje, pisząc tę bibliotekę, a wszystkie projekty przeniosłem na nią. Ponieważ mają one niemal 100% pokrycie testami, oznaczało to pełną funkcjonalność biblioteki.

PyDERASN, podobnie, ma niemal 100% pokrycie testami. Używa generatywnego testowania z wspaniałą biblioteką hypothesis. Przeprowadzono również fuzzing py-afl-em na maszynach z 32 rdzeniami. Mimo że prawie nie mamy kodu Python2, PyDERASN wciąż dba o zgodność z nim i dlatego ma jedynie sześć zależność. Ponadto został przetestowany w stosunku do suite testowa zgodności ASN.1:2008.

Zasada działania jest podobna do pyasn1 — praca z obiektami wysokiego poziomu w Pythonie. Opis schematów ASN.1 jest podobny.

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

Jednak PyDERASN ma podobieństwo do ścisłej typizacji. W pyasn1, jeśli pole miało typ CMSVersion(INTEGER), można było przypisać int lub INTEGER. PyDERASN wymaga, aby przypisywany obiekt był dokładnie CMSVersion. Oprócz tego, że piszemy kod Python3, używamy także adnotacji typu, dlatego w naszych funkcjach będą nie niezrozumiałe argumenty typu def func(serial, contents), a def func(serial: CertificateSerialNumber, contents: EncapsulatedContentInfo), a PyDERASN pomaga utrzymać taki kod.

Przy tym w PyDERASN są niezwykle wygodne udogodnienia tej typizacji. pyasn1 nie pozwalał w SubjectKeyIdentifier().subtype(implicitTag=Tag(…)) przypisywać do pola obiektu SubjectKeyIdentifier() (bez wymaganego IMPLICIT TAG-a) i często trzeba było kopiować i rekonstruować obiekty tylko z powodu zmienionych tóg IMPLICIT/EXPLICIT. PyDERASN surowo dba tylko o podstawowy typ — tagi są automatycznie podstawiane z już istniejącej schemy struktury ASN.1. To znacznie upraszcza kod aplikacji.

Jeśli wystąpi błąd podczas dekodowania, to w pyasn1 trudno jest zrozumieć, gdzie dokładnie się on pojawił. Na przykład w już wcześniej wspomnianym tureckim certyfikacie otrzymamy taki błąd: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (at 138) unsatisfied bounds: 1 ⇐ 77 ⇐ 64 Przy pisaniu struktur ASN.1 ludzie mogą się mylić, a to pomaga łatwiej debugować aplikacje lub wyjaśniać problemy z zakodowanymi dokumentami drugiej strony.

W pierwszej wersji PyDERASN nie było wsparcia dla kodowania BER. Pojawiło się o wiele później i do tej pory nie jest wspierane przetwarzanie UTCTime/GeneralizedTime z przesunięciem czasowym. To przyjdzie w przyszłości, ponieważ projekt pisany jest głównie w wolnym czasie od pracy.

W pierwszej wersji nie było jeszcze obsługi pól DEFINED BY. Po kilku miesiącach ta funkcja się pojawiła i zaczęła być aktywnie wykorzystywana, znacznie skracając kod aplikacji — w wyniku jednej operacji dekodowania można było uzyskać całą strukturę rozłożoną na najgłębszym poziomie. W tym celu w schemie określa się, które pola co "określają". Na przykład, opis schemy 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))),
    )

mówi o tym, że jeśli contentType będzie zawierać OID o wartości id_signedData, to pole content (znajdujące się w tej samej SEQUENCE) należy dekodować według schemy SignedData. Dlaczego tyle nawiasów? Pole może "określać" kilka pól jednocześnie, jak to bywa w strukturach EnvelopedData. Określane pola identyfikowane są przez tak zwany decode path — wskazuje on dokładne umiejscowienie dowolnego elementu we wszystkich strukturach.

Nie zawsze chce się lub nie zawsze jest możliwe od razu wprowadzić te definicje do schemy. Mogą wystąpić przypadki specyficzne dla aplikacji, kiedy OID-y i struktury są znane tylko w zewnętrznym projekcie. PyDERASN umożliwia określenie tych definicji bezpośrednio w momencie dekodowania struktury:

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

Tutaj mówimy, że w CMS SignedData dla wszystkich załączonych certyfikatów należy dekodować wszystkie ich rozszerzenia (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool itp.). Wskazujemy przez decode path, jakiemu elementowi należy "podstawić" definicje, jakby były one zadane w schemie.

Wreszcie, PyDERASN ma możliwość pracy z wiersza poleceń do dekodowania plików ASN.1 i ma bogate pretty printing. Można dekodować dowolne ASN.1 lub określić dokładnie zdefiniowaną schemę i zobaczyć coś takiego:

PyDERASN: jak napisałem bibliotekę ASN.1 z slots i blobs

Informacje wyświetlane: przesunięcie obiektu, długość taga, długość treści, obecność EOC (koniec octetów), wskaźnik kodowania BER, wskaźnik kodowania o nieokreślonej długości, długość i przesunięcie tagu EXPLICIT (jeśli jest), głębokość zagnieżdżenia obiektu w strukturach, wartość IMPLICIT/EXPLICIT tagu, nazwa obiektu w schemacie, jego podstawowy typ ASN.1, numer porządkowy w SEQUENCE/SET OF, wartość CHOICE (jeśli jest), przyjazna dla człowieka nazwa INTEGER/ENUMERATED/BIT STRING w schemacie, wartość dowolnego podstawowego typu, flaga DEFAULT/OPTIONAL z schematu, wskazanie, że obiekt został automatycznie zdekodowany jako DEFINED BY oraz z jakiego OID to wynika, czytelny OID.

System pretty printingu został zaprojektowany tak, aby generować sekwencję obiektów PP, które są już wizualizowane przez osobne narzędzia. Na zrzucie ekranu pokazano renderer w prostym kolorowym tekście. Istnieją również renderery w formacie JSON/HTML, aby można było to zobaczyć z podświetleniem w przeglądarce ASN.1 jak w asn1js projekt.

Inne biblioteki

To nie było celem, ale PyDERASN okazał się znacznie szybszy niż pyasn1. Na przykład dekodowanie plików CRL o rozmiarze megabajtów może trwać tak długo, że trzeba pomyśleć o szybkich formatach pośrednich i zmienić architekturę aplikacji. pyasn1 dekoduje CRL CACert.org na moim laptopie ponad 20 minut, podczas gdy PyDERASN zajmuje tylko 28 sekund! Istnieje projekt asn1crypto, skoncentrowany na szybkiej pracy z struktura mi kryptograficznymi: dekoduje (całkowicie, nie leniwie) ten sam CRL w 29 sekund, chociaż zużywa prawie dwa razy więcej pamięci operacyjnej przy uruchomieniu pod Python3 (983 MiB w porównaniu do 498), a w 3.5 razy więcej pod Python2 (1677 w porównaniu do 488), podczas gdy pyasn1 zużywa aż 4.3 razy więcej (2093 w porównaniu do 488).

asn1crypto, o którym wspomniałem, nie był rozważany, ponieważ projekt był wciąż w fazie rozwoju, a nie słyszeliśmy o nim. Teraz również nie byśmy się w niego zagłębiali, gdyż sam zauważyłem, że ten sam GeneralizedTime nie przyjmuje dowolnego formatu, a podczas serializacji milcząco pomija ułamki sekund. Jest to akceptowalne w kontekście pracy z certyfikatami X.509, ale ogólnie nie nada się.

Na chwilę obecną, PyDERASN jest najbardziej rygorystycznym z darmowych dekoderów DER w Pythonie/Go, które znam. W bibliotece encoding/asn1 mojego ulubionego Go brak jest rygorystycznej kontroli. IDENTYFIKATOR OBIEKTU i UTCTime/GeneralizedTime. Czasami surowość może przeszkadzać (przede wszystkim z powodu kompatybilności wstecznej z przestarzałymi aplikacjami, których nikt nie zamierza poprawiać), dlatego w PyDERASN podczas dekodowania można przekazywać różne ustawienia osłabiające kontrole.

Kod projektu dąży do maksymalnej prostoty. Cała biblioteka to jeden plik. Kod został napisany z myślą o łatwości zrozumienia, bez zbędnych optymalizacji wydajności i DRY-kodu. Nie ma w nim, jak już wspominałem, wsparcia dla pełnego dekodowania BER UTCTime/GeneralizedTime, a także REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING typów danych. W każdym innym przypadku osobiście nie widzę sensu używać w Pythonie innych bibliotek.

Jak wszystkie moje projekty, typu PyGOST, GoGOST, NNCP, GoVPN, PyDERASN jest całkowicie oprogramowaniem typu open source, rozpowszechnianym na warunkach LGPLv3+, i jest dostępny do darmowego pobrania. Przykłady użycia można znaleźć w tutaj i w testach PyGOST.

Siergiej Matwiejew, szifropank, członek Fundacji SPO, programista Python/Go, główny specjalista FGUP „NTC „Atlas”.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster