PyDERASN: hvordan jeg skrev et ASN.1-bibliotek med slots og klatter

ASN.1 dette er en standard (ISO, ITU-T, GOST) for et sprog, der beskriver struktureret information, samt regler for kodning af denne information. For mig, som programmør, er dette blot endnu et serialiserings- og datapræsentationsformat sammen med JSON, XML, XDR og andre. Det er ekstremt almindeligt i vores hverdag, og mange mennesker støder på det: i mobiltelefon, telefon, VoIP-kommunikation (UMTS, LTE, WiMAX, SS7, H.323), i netværksprotokoller (LDAP, SNMP, Kerberos), i alt, hvad der vedrører kryptografi (X.509, CMS, PKCS standarder), i bankkort og biometriske pas og mange andre steder.

Denne artikel diskuterer PyDERASN: Python ASN.1 bibliotek bruges aktivt i projekter relateret til kryptografi i Atlas.

PyDERASN: hvordan jeg skrev et ASN.1-bibliotek med slots og klatter
Generelt anbefales ASN.1 ikke til kryptografiske opgaver: ASN.1 og dets codecs er komplekse. Det betyder, at koden ikke vil være enkel, og dette er altid en ekstra angrebsvektor. Nok at se til listen sårbarheder i ASN.1-biblioteker. Bruce Schneier i sin Kryptografiteknik fraråder også brugen af ​​denne standard på grund af dens kompleksitet: "Den bedst kendte TLV-kodning er ASN.1, men den er utrolig kompleks, og vi viger tilbage fra den". Men det har vi desværre i dag offentlige nøgleinfrastrukturer som aktivt bruger X.509 certifikater, CRL, OCSP, TSP, CMP protokoller, CMC, Beskeder CMSog en masse standarder PKCS. Derfor skal du kunne arbejde med ASN.1, hvis du laver noget relateret til kryptografi.

ASN.1 kan kodes på en række forskellige måder/codecs:

  • BER (Grundlæggende kodningsregler)
  • CER (kanoniske kodningsregler)
  • DER (Distinguished Encoding Rules)
  • GSER (Generiske strengkodningsregler)
  • JER (JSON-kodningsregler)
  • LWER (Light Weight Encoding Rules)
  • OER (Oktetkodningsregler)
  • PER (pakkede kodningsregler)
  • SER (Signalspecifikke kodningsregler)
  • DISCIPLE (XML-kodningsregler)

og en række andre. Men i kryptografiske opgaver bruges to i praksis: BER og DER. Selv i signerede XML-dokumenter (XMLDSig, XADES) vil stadig være Base64-kodede ASN.1 DER-objekter, ligesom i en JSON-orienteret protokol ACME fra Let's Encrypt. Du kan bedre forstå alle disse codecs og principperne for BER / CER / DER-kodning i artikler og bøger: ASN.1 i enkle ord, ASN.1 - Kommunikation mellem heterogene systemer af Olivier Dubuisson, ASN.1 Komplet af Prof John Larmouth.

BER er et binært byte-orienteret (for eksempel PER, populært i cellulær kommunikation - bit-orienteret) TLV-format. Hvert element er kodet som: tag (Tag) identificere typen af ​​element, der kodes (heltal, streng, dato osv.), længde (Length) af indholdet og selve indholdet (Value). BER tillader valgfrit, at en længdeværdi udelades ved at angive en speciel ubestemt længdeværdi og afslutte End-Of-Octets-meddelelsen med en etiket. Ud over længdekodning har BER en masse variation i måden datatyper kodes på, såsom:

  • HELTAL, OBJECT IDENTIFIER, BIT STRING og elementlængde kan være denormaliseret (ikke minimalt kodet);
  • BOOLEAN gælder for ethvert indhold, der ikke er nul;
  • BIT STRING kan indeholde "ekstra" nul bits;
  • BIT STRING, OCTET STRING og alle deres afledte strengtyper, inklusive dato/klokkeslæt, kan opdeles i stykker (chunk) af variabel længde, hvis længde ikke kendes på forhånd under (af)kodning;
  • UTCTime/GeneralizedTime kan have forskellige måder at indstille tidszoneforskydningen på og "ekstra" nul brøkdele af et sekund;
  • DEFAULT SEQUENCE-værdier er muligvis eller måske ikke kodet;
  • De navngivne værdier af de sidste bits i en BIT STRING kan eventuelt efterlades ukodet;
  • SEKVENS (OF)/SET (OF) kan have en vilkårlig rækkefølge af elementer.

På grund af alt ovenstående er det ikke altid muligt at indkode data, så de er identiske med den oprindelige form. Derfor blev et undersæt af regler opfundet: DER - strengt regulerer kun én gyldig kodningsmetode, hvilket er kritisk for kryptografiske opgaver, hvor f.eks. ændring af en bit vil ugyldiggøre signaturen eller kontrolsummen. DER har en væsentlig ulempe: længden af ​​alle elementer skal være kendt på forhånd på indkodningstidspunktet, hvilket ikke tillader streamserialisering af data. CER-codec'et er fri for denne mangel og garanterer ligeledes en utvetydig repræsentation af dataene. Desværre (eller heldigvis har vi ikke endnu mere sofistikerede dekodere?), blev den ikke populær. Derfor støder vi i praksis på en "blandet" brug af BER- og DER-kodede data. Da både CER og DER er en delmængde af BER, kan enhver BER-dekoder behandle dem.

Problemer med pyasn1

På arbejdet skriver vi en masse Python-programmer relateret til kryptografi. Og for et par år siden var der praktisk talt intet valg af gratis biblioteker: enten er disse biblioteker på meget lavt niveau, der giver dig mulighed for blot at indkode / afkode, for eksempel et heltal og en strukturoverskrift, eller også er dette et bibliotek pyasn1. Vi levede på det i flere år og var først meget tilfredse, da det giver dig mulighed for at arbejde med ASN.1-strukturer som med objekter på højt niveau: for eksempel giver et afkodet X.509-certifikatobjekt dig adgang til dets felter gennem en ordbogsgrænseflade: cert["tbsCertificate"] ["serialNumber"] vil vise os serienummeret på dette certifikat. På samme måde kan du "samle" komplekse objekter ved at arbejde med dem som med lister, ordbøger og derefter blot kalde funktionen pyasn1.codec.der.encoder.encode og få en serialiseret repræsentation af dokumentet.

Der blev dog afsløret mangler, problemer og begrænsninger. Der var og er desværre stadig fejl i pyasn1: i skrivende stund er en af ​​de grundlæggende typer i pyasn1 GeneralizedTime, forkert afkodet og kodet.

I vores projekter, for at spare plads, gemmer vi ofte kun filstien, forskydningen og bytelængden for det objekt, vi vil henvise til. For eksempel vil en vilkårlig signeret fil højst sandsynligt være placeret i en CMS SignedData ASN.1-struktur:

  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

og vi kan få den originale signerede fil ved offset 65 bytes, længde 751 bytes. pyasn1 gemmer ikke denne information i sine afkodede objekter. Den såkaldte TLVSeeker blev skrevet - et lille bibliotek, der giver dig mulighed for at afkode tags og længder af objekter, i hvis grænseflade vi kommanderede "gå til næste tag", "gå ind i tagget" (gå ind i SEKVENSEN af objekt), "gå til næste tag", "fortæl din offset og længden af ​​objektet, hvor vi er." Dette var en "manuel" gennemgang af ASN.1 DER-serialiserede data. Men det var umuligt at arbejde med BER-serialiserede data på denne måde, da for eksempel bytestrengen OCTET STRING kunne kodes som flere chunks.

En anden ulempe ved vores pyasn1-opgaver er manglende evne til at forstå ud fra de afkodede objekter, om det givne felt var til stede i SEQUENCE eller ej. For eksempel, hvis strukturen indeholder feltet Felt SEQUENCE OF Smth VALGFRI, så kan det være fuldstændig fraværende i de indgående data (VALGFRI), eller det kan være til stede, men samtidig være af nul længde (en tom liste). I det generelle tilfælde kunne dette ikke findes ud af. Og dette er nødvendigt for en streng kontrol af gyldigheden af ​​de indgående data. Forestil dig, at en eller anden certificeringsmyndighed ville udstede et certifikat med "ikke helt" gyldige data set fra ASN.1-skemaernes synspunkt! For eksempel gik certificeringscentret "TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı" i sit rodcertifikat ud over det tilladte RFC 5280 grænser for længden af ​​emnekomponenten - den kan ikke ærligt afkodes i henhold til skemaet. DER-codec'en kræver, at et felt, hvis værdi er lig med DEFAULT, ikke kodes under transmission - sådanne dokumenter støder på i livet, og den første version af PyDERASN tillod endda bevidst sådan ugyldig (set fra DER-synspunktet) adfærd for skyld. bagudkompatibilitet.

En anden begrænsning er manglende evne til nemt at finde ud af, i hvilken form (BER / DER) dette eller det objekt i strukturen blev kodet. For eksempel siger CMS-standarden, at beskeden er BER-kodet, men feltet signedAttrs, som den kryptografiske signatur er dannet over, skal være i DER. Hvis vi afkoder med DER, så vil vi falde på behandlingen af ​​selve CMS'et, hvis vi afkoder med BER, så ved vi ikke i hvilken form signedAttrs var. Som et resultat bliver TLVSeeker (hvis analog ikke er i pyasn1) nødt til at lede efter placeringen af ​​hvert af de signedAttrs-felter og afkode det separat og tage det fra den serialiserede repræsentation med DER.

Muligheden for automatisk at behandle DEFINED BY-felter, som er meget almindelige, var meget velkommen for os. Efter afkodning af en ASN.1-struktur kan vi stå tilbage med et sæt ALLE felter, der skal behandles yderligere i henhold til det valgte skema baseret på OBJECT IDENTIFIER angivet i strukturfeltet. I Python-kode betyder det at skrive hvis og derefter kalde dekoderen for ETHVERT felt.

Fremkomsten af ​​PyDERASN

Hos Atlas sender vi jævnligt patches opstrøms, når vi finder problemer eller forbedrer den gratis software, vi bruger. I pyasn1 indsendte vi forbedringer flere gange, men pyasn1-koden er ikke den nemmeste at forstå, og nogle gange var der inkompatible API-ændringer, der ramte os. Derudover er vi vant til at skrive test med generativ test, hvilket ikke var tilfældet i pyasn1.

En skønne dag besluttede jeg mig for, at det var nok til at holde ud, og det er tid til at prøve at skrive mit eget bibliotek med __slot__s, offsets og smukt gengivede klatter! Det ville ikke være nok bare at oprette et ASN.1-codec - vi skal overføre alle vores afhængige projekter til det, og det er hundredtusindvis af linjer kode, der er fulde af arbejde med ASN.1-strukturer. Det vil sige et af kravene til det: let oversættelse af den nuværende pyasn1-kode. Efter at have brugt hele min ferie skrev jeg dette bibliotek, overførte alle projekter til det. Da de har næsten 100% dækning af test, betød det, at biblioteket var fuldt funktionelt.

PyDERASN har på samme måde næsten 100% testdækning. Bruger generativ test med et vidunderligt bibliotek hypotese. Også holdt og fnugdannelse py-afl-spise på 32 nukleare maskiner. På trods af at vi praktisk talt ikke har nogen Python2-kode tilbage, observerer PyDERASN stadig kompatibilitet med den og har på grund af dette den eneste seks afhængighed. Desuden er det blevet testet mod ASN.1:2008 overholdelsestestpakke.

Princippet om at arbejde med det ligner pyasn1 - at arbejde med Python-objekter på højt niveau. Beskrivelsen af ​​ASN.1-skemaer er ens.

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

PyDERASN har dog et udseende af stærk skrivning. I pyasn1, hvis et felt var af typen CMSVersion(INTEGER), så kunne det tildeles int eller INTEGER. PyDERASN kræver strengt, at det tildelte objekt er nøjagtigt CMSVersion. Udover at skrive Python3-kode, bruger vi også skrive anmærkninger, så vores funktioner vil ikke have obskure argumenter som def func(serial, contents), men def func(serial: CertificateSerialNumber, contents: EncapsulatedContentInfo), og PyDERASN hjælper med at observere en sådan kode.

Samtidig har PyDERASN ekstremt bekvemme aflad til netop denne skrivning. pyasn1 tillod ikke i feltet SubjectKeyIdentifier().subtype(implicitTag=Tag(…)) at tildele et objekt til SubjectKeyIdentifier() (uden det påkrævede IMPLICIT TAG) og måtte ofte kun kopiere og genskabe objekter på grund af ændret IMPLICIT/ EKSPLICIT tags. PyDERASN observerer strengt kun basistypen - den vil automatisk erstatte tags fra strukturens allerede eksisterende ASN.1-skema. Dette forenkler applikationskoden betydeligt.

Hvis der opstår en fejl under afkodningen, er det ikke nemt for pyasn1 at forstå præcis, hvor den opstod. For eksempel vil vi i det tyrkiske certifikat, der allerede er nævnt ovenfor, få følgende fejl: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (ved 138) uopfyldte grænser: 1 ⇐ 77 ⇐ 64 Når man skriver ASN .1-strukturer, kan folk lave fejl, og det hjælper lettere at fejlsøge applikationer eller finde ud af problemer i kodede dokumenter på den anden side.

Den første version af PyDERASN understøttede ikke BER-kodning. Det dukkede op meget senere og understøtter stadig ikke UTCTime / GeneralizedTime-behandling med tidszoner. Dette vil komme i fremtiden, fordi projektet er skrevet primært i deres fritid.

Også i den første version var der ikke arbejde med DEFINED BY-felter. Et par måneder senere dette muligheden opstod og begyndte at blive brugt aktivt, hvilket reducerede applikationskoden betydeligt - i en afkodningsoperation var det muligt at få hele strukturen adskilt til dybden. For at gøre dette, i skemaet, hvilke felter "definerer", hvad der er indstillet. For eksempel en beskrivelse af CMS-skemaet:

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

siger, at hvis contentType indeholder en OID med værdien id_signedData, så skal indholdsfeltet (placeret i samme SEQUENCE) afkodes i henhold til SignedData-skemaet. Hvorfor så mange parenteser? Et felt kan "definere" flere felter på samme tid, som det er tilfældet med EnvelopedData-strukturer. Definerede felter identificeres af den såkaldte afkodningssti - den angiver den nøjagtige placering af ethvert element i alle strukturer.

Det er ikke altid ønskeligt eller ikke altid muligt umiddelbart at tilføje disse definitioner til ordningen. Der kan være applikationsspecifikke tilfælde, hvor OID'er og strukturer kun kendes i et tredjepartsprojekt. PyDERASN giver mulighed for at indstille disse definerer lige på tidspunktet for afkodning af strukturen:

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

Her siger vi, at i CMS SignedData for alle vedhæftede certifikater, afkode alle deres udvidelser (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool osv.). Vi angiver gennem afkodningsstien, hvilket element der skal "substitueres" med definerer, som om det var sat i skemaet.

Endelig har PyDERASN mulighed for at arbejde ud fra kommandolinje til afkodning af ASN.1-filer og har en rig smukt tryk. Du kan afkode en vilkårlig ASN.1, eller du kan indstille et veldefineret skema og se noget som dette:

PyDERASN: hvordan jeg skrev et ASN.1-bibliotek med slots og klatter

Vist information: objektoffset, taglængde, længdelængde, indholdslængde, tilstedeværelse af EOC (end-of-octets), BER-kodningsflag, uendelig længde kodningsflag, EXPLICIT taglængde og -offset (hvis nogen), objektnesting-dybde i strukturer, IMPLICIT/EXPLICIT tag-værdi, skemanavn på objekt, dets basis-ASN.1-type, ordinal inden for SEQUENCE/SET OF, VALG-værdi (hvis nogen), human-læsbart skemanavn INTEGER/ENUMERATED/BIT STRING, værdi af enhver base type , DEFAULT/VALGFRI flag fra skemaet, et tegn på, at objektet automatisk blev afkodet som DEFINERET AF og på grund af hvilken OID dette skete, en menneskelig læsbar OID.

Det smukke printsystem er specielt fremstillet på en sådan måde, at det genererer en sekvens af PP-objekter, der allerede er gengivet adskilte. Skærmbilledet viser rendereren i almindelig farvet tekst. Der findes også renderere i JSON/HTML-format, så det kan ses med fremhævelse i ASN.1-browseren som i asn1js projekt.

Andre biblioteker

Dette var ikke målet, men PyDERASN viste sig at være væsentlig hurtigere end pyasn1. For eksempel kan afkodning af CRL-filer i megabyte-størrelser tage så lang tid, at du skal tænke på mellemliggende datalagringsformater (hurtigt) og ændre applikationernes arkitektur. pyasn1 afkoder CRL CACert.org på min bærbare computer i over 20 minutter, mens PyDERASN kun tager 28 sekunder! Der er et projekt asn1crypto, rettet mod hurtigt arbejde med kryptografiske strukturer: den afkoder (helt, ikke dovent) den samme CRL på 29 sekunder, men bruger næsten dobbelt så meget RAM, når den kører under Python3 (983 MiB mod 498), og på 3.5 gange under Python2 (1677) vs 488), mens pyasn1 forbruger så meget som 4.3 gange mere (2093 vs 488).

asn1crypto, som jeg nævnte, overvejede vi ikke, fordi projektet stadig var i sin vorden, og vi havde ikke hørt om det. Nu ville de heller ikke se i hans retning, da jeg straks fandt ud af, at den samme GeneralizedTime ikke antager en vilkårlig form, og under serialisering fjerner den lydløst en brøkdel af et sekund. Dette er acceptabelt for at arbejde med X.509-certifikater, men generelt vil det ikke fungere.

I øjeblikket er PyDERASN den mest strenge gratis Python/Go DER-dekoder, jeg kender. I encoding/asn1-biblioteket i min favorit Go ingen streng kontrol OBJECT IDENTIFIER og UTCTime/GeneralizedTime-strenge. Nogle gange kan strenghed komme i vejen (primært på grund af bagudkompatibilitet med gamle applikationer, som ingen vil rette), så i PyDERASN under afkodning kan du bestå forskellige indstillinger svækkende kontroller.

Projektkoden forsøger at være så enkel som muligt. Hele biblioteket er én fil. Koden er skrevet med vægt på let at forstå, uden unødvendige ydelsesoptimeringer og DRY-kode. Det understøtter ikke, som jeg allerede sagde, fuldgyldig BER-afkodning af UTCTime / GeneralizedTime-strenge, såvel som REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING datatyper. I alle andre tilfælde ser jeg personligt ingen grund til at bruge andre biblioteker i Python.

Som alle mine projekter, f.eks PyGOST, GoGOST, NCCP, GoVPN, PyDERASN er fuldstændig gratis softwarefordelt på vilkårene LGPLv3+, og er tilgængelig til gratis download. Der er eksempler på brug her og PyGOST tests.

Sergey Matveev, cypherpunk, medlem SPO Fonden, Python/Go-udvikler, chefspecialist Federal State Unitary Enterprise "STC "Atlas".

Kilde: www.habr.com

Tilføj en kommentar