PyDERASN: hvordan jeg skrev en ASN.1-bibliotek med slots og blobs

ASN.1 er en standard (ISO, ITU-T, GOST) for et sprog, der beskriver struktureret information samt reglerne for kodning af denne information. For mig som programmør er det blot endnu et format til serialisering og præsentation af data, på niveau med JSON, XML, XDR og andre. Det er ekstremt udbredt i vores dagligdag, og mange støder på det: i mobil-, telefon- og VoIP-kommunikation (UMTS, LTE, WiMAX, SS7, H.323), i netværksprotokoller (LDAP, SNMP, Kerberos), i alt, der handler om kryptografi (X.509, CMS, PKCS-standarder), i betalingskort og biometriske pas, og mange andre steder.

Denne artikel handler om PyDERASN: Python ASN.1 bibliotek anvendt aktivt i projekter relateret til kryptografi i Atlas.

PyDERASN: hvordan jeg skrev en ASN.1-bibliotek med slots og blobs
Generelt set er det ikke en god idé at anbefale ASN.1 til kryptografiske opgaver: ASN.1 og dets kodeker er komplekse. Det betyder, at koden ikke vil være simpel, hvilket altid er en yderligere angrebsvektor. Det er nok at se på listen over sårbarheder i ASN.1-biblioteker. Bruce Schneier anbefaler også ikke at bruge denne standard på grund af dens kompleksitet i sin Cryptography engineering , der også advarer imod brugen af denne standard på grund af dens kompleksitet: "Den mest kendte TLV-kodning er ASN.1, men den er utrolig kompleks, og vi undgår den." Men desværre har vi i dag infrastrukturer til offentlige nøgler hvor der aktivt bruges X.509 certifikater, CRL, OCSP, TSP, CMP-protokoller, CMC, meddelelser CMS, og en masse standarder PKCS. Derfor er det nødvendigt at kunne arbejde med ASN.1, hvis du beskæftiger dig med noget relateret til kryptografi.

ASN.1 kan kodes på mange måder/kodeker:

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

og en række andre. Men i kryptografiske opgaver bruges der i praksis to: BER og DER. Selv i signerede XML-dokumenter (XMLDSig, XAdES) vil der stadig være Base64-kodede ASN.1 DER objekter, ligesom i det JSON-orienterede protokol ACME fra Let’s Encrypt. Det er bedre at sætte sig ind i alle disse kodeker og kodningsprincipper BER/CER/DER, som man kan finde i artikler og bøger: ASN.1 i enkle termer, ASN.1 – Kommunikation mellem heterogene systemer af Olivier Dubuisson, ASN.1 Complete af Prof John Larmouth.

BER er et binært byte-orienteret (for eksempel PER, populært i mobilkommunikation – bit-orienteret) TLV-format. Hvert element kodes som: et tag (Tag), der identificerer typen af den kodede enhed (heltal, streng, dato, osv.), længde (Længde) af indholdet og selve indholdet (Vværdi). BER tillader valgfrit ikke at angive længdeværdien ved at sætte en særlig indefinit længde værdi og afslutte med et End-Of-Octets mærke. Udover længdekodning er der stor variation i måden at kode datatyper på, som for eksempel:

  • INTEGER, OBJECT IDENTIFIER, BIT STRING og længden af elementet kan være unormeret (ikke kodet i minimal form);
  • BOOLEAN er sand, når indholdet er non-zero;
  • BIT STRING kan indeholde "overflødige" nul-bits;
  • BIT STRING, OCTET STRING og alle deres afledte type-strenge, inklusive dato/tid, kan opdeles i variable længde stykker, hvis længde under (de)kodning ikke er kendt på forhånd;
  • UTCTime/GeneralizedTime kan have forskellige måder at angive tidszone-offset og "overflødige" nul-delssekunder;
  • DEFAULT værdier for SEQUENCE kan kodificeres, men det er ikke nødvendigt;
  • Navngivne værdier for de sidste bits i BIT STRING kan valgfrit undgås ved kodning;
  • SEQUENCE (OF)/SET (OF) kan have vilkårlig rækkefølge af elementer.

På grund af alt det ovenstående er det ikke altid muligt at kode data, så de er identiske med den originale form. Derfor blev der opfundet et delmængde af regler: DER — strengt regulerende kun én tilladt måde at kode på, hvilket er kritisk for kryptografiske opgaver, hvor for eksempel ændring af et enkelt bit vil gøre en signatur eller checksum ugyldig. DER har en væsentlig ulempe: længden af alle elementer skal være kendt på forhånd under kodning, hvilket ikke tillader streaming serialisering af data. CER codec har ikke denne ulempe og garanterer samtidig en unik præsentation af data. Desværre (eller heldigvis, at vi ikke har endnu mere komplekse dekodere?) er det ikke blevet populært. Derfor møder vi i praksis en "blandet" brug af BER og DER kodede data. Da både CER og DER er delmængder af BER, kan enhver BER-dekoder håndtere dem.

Problemer med pyasn1

På arbejdet skriver vi mange programmer i Python relateret til kryptografi. For flere år siden var der næsten ingen frit tilgængelige biblioteker: enten er det meget lavniveau biblioteker, der blot gør det muligt at kode/dekode, såsom at kodificere et helt tal og en strukturheader, eller også er det et bibliotek pyasn1. Vi brugte det i flere år og var i starten meget tilfredse, da det gjorde det muligt at arbejde med ASN.1 strukturer som med højniveau objekter: for eksempel tillader et dekodet X.509 certifikat, at vi tilgår dets felter gennem en ordbog-agtig grænseflade: cert["tbsCertificate"]["serialNumber"] viser os dette certifikats serienummer. På samme måde kan man "samle" komplekse objekter ved at arbejde med dem som lister, ordbøger, og derefter blot kalde funktionen pyasn1.codec.der.encoder.encode for at få den serialiserede repræsentation af dokumentet.

Men der opstod ulemper, problemer og begrænsninger. Der var og, desværre, er der stadig fejl i pyasn1: på tidspunktet for skrivningen, i pyasn1 er en af de grundlæggende typer - GeneralizedTime, forkert dekodet og kodet.

I vores projekter, for at spare plads, gemmer vi ofte kun stien til filen, offset og længden i byte af objektet, som vi ønsker at referere til. For eksempel vil en vilkårlig underskreven fil sandsynligvis befinde sig i CMS SignedData ASN.1 strukturen:

  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

                 HER INDHOLDER INDHOLDET AF DEN UNDERSKREVNE FIL PÅ 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

Vi kan få fat i den originale underskrevne fil med et offset på 65 byte og en længde på 751 byte. pyasn1 gemmer ikke disse oplysninger i sine dekodede objekter. Der blev skrevet et såkaldt TLVSeeker — et lille bibliotek, der gør det muligt at dekode tags og længder af objekter, hvor vi i grænsefladen kunne give kommandoer som „gå til næste tag“, „gå ind i tagget“ (går ind i SEQUENCE objektet), „gå til næste tag“, „fortæl om dit offset og længden af objektet, hvor vi befinder os“. Det var en „manuelt“ vandring gennem ASN.1 DER-serialiserede data. Men dette kunne ikke bruge arbejdet med BER-serialiserede data, da en byte-streng, OCTET STRING, for eksempel kunne være kodet i flere chunk-ov.

En anden ulempe ved pyasn1 til vores opgaver er manglende evne til at forstå, om et givet felt var til stede i SEQUENCE eller ej baseret på de dekodede objekter. For eksempel, hvis strukturen indeholder feltet Field SEQUENCE OF Smth OPTIONAL, kunne det helt mangle i de modtagne data (OPTIONAL), eller det kunne være til stede, men have en længde på nul (en tom liste). Generelt kunne dette ikke fastslås. Og det er nødvendigt for strikte validering af de modtagne data. Forestil dig, at en certifikationsmyndighed udstedte et certifikat med data, der ikke helt var valide i henhold til ASN.1-scheme! For eksempel, certifikationsmyndigheden „TÜRKTRUST Elektronik Sertifika Hizmet Sağlayıcısı“ overskred i sin rodcertifikat de tilladte RFC 5280 grænser for længden på komponenten subject — den kan ikke ærligt dekodes efter ordningen. DER-koderen kræver, at feltet, hvis værdi er DEFAULT-værdien, ikke kodificeres under transmission — sådanne dokumenter findes i virkeligheden, og den første version af PyDERASN accepterede endda sådan ikke-valide (i henhold til DER) opførsel med vilje for at sikre bagudkompatibilitet.

En anden begrænsning er, at det er vanskeligt at finde ud af, hvilken form (BER/DER) et specifikt objekt er kodet i strukturen. For eksempel siger CMS-standarden, at meddelelsen skal kodes i BER, men feltet signedAttrs, som den kryptografiske signatur dannes over, skal være i DER. Hvis vi dekoder med DER, vil vi støde på problemer med selve CMS-behandlingen, og hvis vi dekoder med BER, ved vi ikke, hvilken form signedAttrs er i. Det ender med, at vi er nødt til at bruge TLVSeeker (et værktøj, som der ikke findes en ækvivalent til i pyasn1) til at finde placeringen af hvert af signedAttrs-felterne og derefter dekode dem separat fra det serialiserede format i DER.

Det var meget ønskeligt for os at have muligheden for automatisk at behandle DEFINED BY-felter, som ofte findes. Efter dekodning af ASN.1-strukturen kan vi ende med mange ANY-felter, der skal behandles videre i henhold til den skema, der vælges baseret på OBJECT IDENTIFIER angivet i strukturens felt. I Python-koden betyder det, at vi skal skrive if-sætninger og derefter kalde dekoderen for ANY-feltet.

Fremkomsten af PyDERASN

I Atlas sender vi regelmæssigt patches op, når vi finder problemer eller forbedrer de anvendte open source-programmer. I pyasn1 har vi flere gange sendt forbedringer, men pyasn1-koden er ikke den letteste at forstå, og der har nogle gange været inkompatible ændringer i API'en, der har givet os problemer. Desuden er vi vant til at skrive tests med generativ testning, hvilket ikke var i pyasn1.

En dag bestemte jeg, at jeg havde fået nok og ville forsøge at skrive mit eget bibliotek med __slots__, offsets og perfekt præsenterede blobs! Bare at skabe en ASN.1-kodek ville ikke være tilstrækkeligt - jeg skulle oversætte alle vores indbyrdes afhængige projekter til det, hvilket er hundredtusindvis af linjer kode, der har meget arbejde med ASN.1-strukturer. Så et af kravene til det er: lethed ved at oversætte den nuværende pyasn1-kode. Jeg brugte hele min ferie på at skrive dette bibliotek, og jeg omdannede alle projekter til det. Da de har næsten 100%-dækning af tests, betød det også, at biblioteket var fuldt funktionelt.

PyDERASN har, ligesom, næsten 100% testdækning. Generativ testning anvendes med et fremragende bibliotek hypothesis. Der blev også gjort fuzzing py-afl- på 32-core maskiner. Selvom vi praktisk talt ikke har noget Python2-kode tilbage, opretholder PyDERASN stadig kompatibiliteten med den, og derfor har den kun én seks afhængighed. Desuden er den testet imod ASN.1:2008 compliance test suite.

Princippet for at arbejde med den er analogt til pyasn1 — arbejde med høj-niveau Python-objekter. 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)),
    )

Men PyDERASN har en form for strengt typografi. I pyasn1, hvis feltet havde typen CMSVersion(INTEGER), kunne man tildele det en int eller INTEGER. PyDERASN kræver strengt, at det tildelte objekt skal være netop CMSVersion. Desuden, da vi skriver Python3-kode, bruger vi også typing annotations, så i vores funktioner vil der ikke være uklare argumenter som def func(serial, contents), men def func(serial: CertificateSerialNumber, contents: EncapsulatedContentInfo), og PyDERASN hjælper med at opretholde sådan kode.

Samtidig er der i PyDERASN meget bekvemme lettelser ved denne typekontrol. pyasn1 tillod ikke at tildele et SubjectKeyIdentifier().subtype(implicitTag=Tag(…)) felt et SubjectKeyIdentifier() objekt (uden den nødvendige IMPLICIT TAG), og man måtte ofte kopiere og genskabe objekter blot på grund af ændrede IMPLICIT/EXPLICIT tags. PyDERASN opretholder kun den grundlæggende type strengt — tags indsættes automatisk fra det allerede eksisterende ASN.1 skema. Dette gør applikationskoden betydeligt enklere.

Hvis der opstår en fejl under dekoderingen, er det i pyasn1 ikke let at forstå, hvor præcist den opstod. For eksempel, i det allerede nævnte tyrkiske certifikat vil vi få denne fejl: UTF8String (tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String) (ved 138) utilfredse grænser: 1 ⇐ 77 ⇐ 64. Når man skriver ASN.1 strukturer, kan folk begå fejl, og dette hjælper med lettere at debugge applikationer eller finde problemer i de kodede dokumenter fra den modsatte side.

I den første version af PyDERASN var der ikke understøttelse af BER-kodning. Denne kom meget senere og understøtter stadig ikke behandling af UTCTime/GeneralizedTime med tidszoner. Dette vil komme i fremtiden, da projektet først og fremmest skrives i fritiden.

I den første version var der heller ikke funktioner til at arbejde med DEFINED BY felterne. Efter nogle måneder blev denne mulighed tilgængelig og begyndte at blive brugt aktivt, hvilket markant reducerede mængden af kode i applikationerne — med én dekoderingsoperation kunne man få den fulde struktur ned til mindste detalje. For dette angives, hvilke felter der "definerer" hvad i skemaet. For eksempel beskriver skemaet for 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))),
    )

angiver, at hvis contentType indeholder en OID med værdien id_signedData, så skal feltet content (som findes i denne samme SEQUENCE) dekodes i henhold til skemaet SignedData. Hvorfor så mange parenteser? Feltet kan "definere" flere felter på én gang, ligesom det ses i EnvelopedData strukturer. De definerede felter identificeres ved den såkaldte decode path — den angiver den præcise placering af ethvert element i alle strukturer.

Det er ikke altid ønskeligt eller muligt straks at inkludere disse defines i skemaet. Der kan være applikationsspecifikke tilfælde, hvor OID'er og strukturer kun er kendt i et eksternt projekt. PyDERASN giver mulighed for at angive disse defines direkte i det øjeblik, strukturen decodes:

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 angiver vi, at i CMS SignedData skal alle de vedhæftede certifikater have dekodet alle deres udvidelser (AuthorityKeyIdentifier, BasicConstraints, SubjectSignTool osv.). Vi angiver via decode path, hvilket element der skal "indsættes" defines i, som om det var angivet i skemaet.

Endelig har PyDERASN mulighed for at arbejde fra kommandolinjen for at dekode ASN.1 filer og har en rig pretty printing. Man kan dekode vilkårlig ASN.1, eller man kan specificere et klart defineret skema og se noget lignende:

PyDERASN: hvordan jeg skrev en ASN.1-bibliotek med slots og blobs

Vist information: objektets forskydning, taglængde, længden af indholdet, tilstedeværelsen af EOC (end-of-octets), BER-kodningsindikator, indikator for indefinite-length kodning, længde og forskydning af EXPLICIT-tag (hvis der er et), dybden af objektet i strukturer, IMPLICIT/EXPLICIT tagværdi, navnet på objektet i skemaet, dets grundlæggende ASN.1 type, rækkefølgen inden for SEQUENCE/SET OF, værdien i CHOICE (hvis der er en), menneskeligt læsbart navn INTEGER/ENUMERATED/BIT STRING ifølge skemaet, værdien af enhver grundlæggende type, DEFAULT/OPTIONAL flag fra skemaet, indikator for at objektet blev automatisk dekodet som DEFINED BY, og hvilket OID det skete med, menneskeligt læsbart OID.

Systemet til pretty printing er designet til at generere en sekvens af PP-objekter, som visualiseres med separate værktøjer. På screenshot'et vises renderer i enkel farvet tekst. Der findes også renderere i JSON/HTML-format, så man kan se det med syntaksfremhævelse i browseren som i asn1js projektet.

Andre biblioteker

Det var ikke målet, men PyDERASN er blevet væsentligt hurtigere end pyasn1. For eksempel kan dekodning af CRL-filer på megabyte kan tage så lang tid, at man må overveje hurtige mellemformater til datalagring og ændre applikationsarkitektur. pyasn1 dekoder CRL CACert.org på min bærbare computer over 20 minutter, mens PyDERASN kun tager 28 sekunder! Der er et projekt asn1crypto, der har fokus på hurtig håndtering af kryptografiske strukturer: det dekoder (komplet, ikke latente) den samme CRL på 29 sekunder, men bruger næsten det dobbelte af hukommelsen ved kørsel under Python3 (983 MiB mod 498), og 3,5 gange mere under Python2 (1677 mod 488), mens pyasn1 bruger hele 4,3 gange mere (2093 mod 488).

asn1crypto, som jeg nævnte, overvejede vi ikke, fordi projektet stadig var i sin spæde fase, og vi havde ikke hørt om det. I dag ville vi heller ikke kigge i den retning, da jeg straks opdagede, at den samme GeneralizedTime ikke accepterer vilkårlige former, og ved serialisering fjerner den stille sekunderne. Dette er acceptabelt for arbejde med X.509-certifikater, men generelt ikke anvendeligt.

I øjeblikket er PyDERASN den mest stringent af de gratis Python/Go DER-dekodere, jeg kender. I encoding/asn1-biblioteket i mit elskede Go er der ikke streng kontrol OBJKT IDENTIFIER og UTCTime/GeneralizedTime strenge. Nogle gange kan strenge regler være til hinder (primært på grund af bagudkompatibilitet med gamle applikationer, som ingen vil opdatere), derfor kan man i PyDERASN under dekodning angive forskellige indstillinger som lempes.

Projektkoden forsøger at være så simpel som muligt. Hele biblioteket er én fil. Koden er skrevet med fokus på forståelighed, uden unødvendige præstationer og DRY-kode. Som nævnt er der ikke støtte til fuld BER-dekodning af UTCTime/GeneralizedTime strenge, samt REAL, RELATIVE OID, EXTERNAL, INSTANCE OF, EMBEDDED PDV, CHARACTER STRING datatyper. I alle andre tilfælde ser jeg personligt ikke nogen mening i at bruge andre biblioteker i Python.

Som med alle mine projekter, type PyGOST, GoGOST, NNCP, GoVPN, er PyDERASN helt gratis software, der distribueres under betingelserne for LGPLv3+, og er tilgængelig for gratis download. Eksempler på brug er her og i testene i PyGOST.

Sergey Matveev, kryptonørd, medlem af Stiftelsen for fri software, Python/Go-udvikler, hovedspecialist FGUП „NTC „Atlas“.

Kilde: habr.com

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster