PyDERASN:スロットとBLOBを使用してASN.1ライブラリを作成したとき

ASN.1は、構造化された情報を記述する言語の標準(ISO、ITU-T、GOST)、およびこの情報のエンコード規則です。 プログラマーとしての私にとって、これはJSON、XML、XDRなどとともに、データをシリアル化して表示するための単なる別の形式です。 それは私たちの日常生活で非常に一般的であり、多くの人々がそれに遭遇します:携帯電話、電話、VoIP(UMTS、LTE、WiMAX、SS7、H.323)、ネットワークプロトコル(LDAP、SNMP、Kerberos)、すべて暗号化(X.509、CMS、PKCS標準)、銀行カード、生体認証パスポートなどについて。



この記事では、 PyDERASNAtlasの暗号化に関連するプロジェクトで積極的に使用されているPython ASN.1ライブラリについて説明します。



私自身






実際、暗号化タスクにASN.1を推奨する価値はありません。ASN.1とそのコーデックは複雑です。 これは、コードが単純ではないことを意味しますが、常に追加の攻撃ベクトルになります。 ASN.1ライブラリ脆弱性のリストをご覧ください。 ブルース・シュナイアーは、 暗号工学で、この標準の使用を推奨していません。複雑さのためです:「最もよく知られているTLVエンコードはASN.1ですが、信じられないほど複雑であり、私たちはそれを避けます。」 しかし、残念ながら、今日ではX.509証明書 、CRL、OCSP、TSP、CMP、 CMCCMSメッセージ、および多くのPKCS標準を積極的に使用する公開鍵インフラストラクチャがあります。 したがって、暗号化に関連する何かをしている場合は、ASN.1を操作できる必要があります。



ASN.1は、さまざまな方法/コーデックでエンコードできます。





その他多数。 しかし、実際の暗号化タスクでは、BERとDERの2つが使用されます。 署名されたXMLドキュメント( XMLDSigXAdES )でも、Let's EncryptのJSONベースのACMEプロトコルと同様に、Base64でエンコードされたASN.1 DERオブジェクトが残っています。 これらすべてのコーデックとBER / CER / DERコーディングの原則を記事や本でよりよく理解できます: ASN.1簡単な言葉ASN.1-Olivier Dubuissonによる異種システム間の通信ASN.1 John Larmouth教授が完成



BERは、バイナリバイト指向(たとえば、PER、モバイル通信で一般的-ビット指向)のTLV形式です。 各要素は、エンコードされる要素のタイプ(整数、文字列、日付など)、コンテンツの長さ(長さ)およびコンテンツ自体(値)を識別するタグ( T ag)の形式でエンコードされます。 BERでは、オプションで特別な不定の長さの値を設定し、End-Of-Octetsメッセージラベルで終わることにより、長さの値を指定しないようにすることができます。 長さコーディングに加えて、BERには、次のようなデータ型のエンコード方法に多くのばらつきがあります。





上記のすべてについて、データを元の形式と同一になるようにエンコードすることが常に可能であるとは限りません。 そのため、ルールのサブセットが発明されました。DERは、1つの有効なコーディング方法のみの厳密な規制であり、暗号化タスクにとって重要です。たとえば、1ビットを変更すると署名またはチェックサムが無効になります。 DERには重大な欠点があります。すべての要素の長さは、エンコード中に事前に知っておく必要があり、データのストリームシリアル化を許可しません。 CERコーデックにはこの欠点がなく、同様にデータの明確な表示が保証されます。 残念ながら(または幸いなことに、さらに複雑なデコーダーはありませんか?)、普及しませんでした。 そのため、実際には、BERとDERでエンコードされたデータが「混在」して使用されます。 CERとDERはどちらもBERのサブセットであるため、BERデコーダーはそれらを処理できます。



pyasn1の問題



職場では、暗号化に関連する多くのPythonプログラムを作成しています。 そして数年前、無料のライブラリを選択することは実質的にありませんでした:これらは、例えば整数と構造ヘッダーを単純にエンコード/デコードできる非常に低レベルのライブラリであるか、 pyasn1ライブラリです。 ASN.1構造を高レベルのオブジェクトとして使用できるため、最初は非常に満足していました。たとえば、デコードされたX.509証明書オブジェクトを使用すると、辞書インターフェイスを介してそのフィールドにアクセスできます。cert ["tbsCertificate"] ["SerialNumber"]は、この証明書のシリアル番号を表示します。 同様に、複雑なオブジェクトをリストや辞書のように操作して「収集」し、pyasn1.codec.der.encoder.encode関数を呼び出すだけで、ドキュメントのシリアル化された表現を取得できます。



しかし、欠陥、問題、制限が明らかになりました。 残念ながら、pyasn1にはまだエラーが残っています。執筆時点では、pyasn1では基本タイプの1つであるGeneralizedTimeが誤ってデコードおよびエンコードされています。



プロジェクトでは、スペースを節約するために、参照するオブジェクトのファイルへのパス、オフセット、およびバイト単位の長さのみを保存することがよくあります。 たとえば、任意の署名済みファイルは、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] . . 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
      
      





また、65バイト、751バイトのオフセットで元の署名済みファイルを取得できます。 pyasn1は、デコードされたオブジェクトにこの情報を保存しません。 いわゆるTLVSeekerが作成されました-「次のタグに移動」、「タグの内部に移動」(オブジェクトのシーケンスの内部に移動)、「次のタグに移動」、「オフセットと現在のオブジェクトの長さ。」 これは、ASN.1 DERでシリアル化されたデータの「手動」ウォークでした。 しかし、たとえば、バイト文字列OCTET STRINGは複数のチャンクとしてエンコードされる可能性があるため、このようなBERシリアル化されたデータを扱うことはできませんでした。



pyasn1タスクのもう1つの欠点は、デコードされたオブジェクトから、特定のフィールドがSEQUENCEに存在していたかどうかを理解できないことです。 たとえば、構造体にField SEQUENCE OF Smth OPTIONALフィールドが含まれている場合、受信データには完全に存在しない可能性がありますが(OPTIONAL)、存在する可能性がありますが、長さがゼロ(空のリスト)である可能性があります。 一般的なケースでは、これを明確にすることはできませんでした。 そして、これは受信したデータの有効性の厳密なチェックに必要です。 ASN.1スキームの観点から、一部の証明機関が「完全ではない」有効なデータを含む証明書を発行すると想像してください。 たとえば、ルート証明書の認証機関TÜRKTRUSTElektronik Sertifika HizmetSağlayıcısıは、サブジェクトコンポーネントの長さについて、 RFC 5280の許容範囲を超えています-スキームに従って正直にデコードすることはできません。 DERコーデックは、値がDEFAULTのフィールドが送信中にエンコードされないことを要求します-そのようなドキュメントは人生で遭遇し、PyDERASNの最初のバージョンは下位互換性のためにそのような無効な(DERの観点から)振る舞いさえ意識的に許可しました。



もう1つの制限は、構造内で1つまたは別のオブジェクトがどの形式(BER / DER)でエンコードされているかを簡単に見つけることができないことです。 たとえば、CMS標準では、メッセージはBERエンコードされていますが、暗号化署名が形成されるsignedAttrsフィールドはDERにある必要があるとされています。 DERを使用してデコードする場合、CMS自体の処理が行われます。BERを使用してデコードする場合、signedAttrsがどのような形式であったかはわかりません。 その結果、TLVSeeker(その類似物はpyasn1にはない)がsignedAttrsフィールドのそれぞれの場所を検索する必要があり、シリアル化されたビューからDERによって個別にデコードする必要があります。



非常に一般的なDEFINED BYフィールドの自動処理の可能性は、私たちにとって非常に望ましいものでした。 ASN.1構造をデコードした後、多くのANYフィールドが残っている可能性があり、構造フィールドで指定されたOBJECT IDENTIFIERに基づいて選択されたスキームに従ってさらに処理する必要があります。 Pythonコードでは、これはifを記述してから、ANYフィールドのデコーダーを呼び出すことを意味します。



PyDERASNの出現



アトラスでは、問題を発見したり、使用中の無料プログラムを変更したりして、定期的にパッチをトップに送ります。 pyasn1では、何度か改善を送信しましたが、pyasn1コードは理解するのが最も簡単ではなく、時々互換性のないAPIの変更が発生したため、手に負えませんでした。 さらに、pyasn1には当てはまらなかった生成テストを使用してテストを記述することに慣れています。



ある晴れた日、私はこれに耐えなければならないと決心し、__ slot__s、offset s、および完全に表示されたblobを使用して独自のライブラリを作成するときがきました! ASN.1コーデックを作成するだけでは十分ではありません。すべての依存プロジェクトをそれに転送する必要があります。これは、ASN.1構造で多くの作業を行う数十万行のコードです。 それが要件の1つです。現在のpyasn1コードの翻訳の容易さ。 休暇をすべて過ごした後、このライブラリを作成し、すべてのプロジェクトをそこに移しました。 彼らはテストでほぼ100%をカバーしているため、これはまた、ライブラリが完全に機能していることを意味しました。



同様に、PyDERASNのテスト範囲はほぼ100%です。 生成テストは、素晴らしい仮説ライブラリで使用されます。 また、32台の原子力機械のファジングも実行されました。 Python2コードがほとんど残っていないという事実にもかかわらず、PyDERASNはまだそれとの互換性を維持しており、このため単一の6つの依存関係があります。 さらに、 ASN.1:2008コンプライアンステストスイートに対してテストされています



それを使用する原理は、pyasn1-高レベルのPythonオブジェクトを使用することと似ています。 ASN.1回線の説明も同様です。



 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には強い型付けのように見えます。 pyasn1では、フィールドのタイプがCMSVersion(INTEGER)の場合、intまたはINTEGERを割り当てることができます。 PyDERASNでは、割り当てられたオブジェクトがCMSVersionであることが厳密に要求されます。 Python3コードの記述に加えて、 タイピングアノテーションを使用するため、関数にはdef func(シリアル、コンテンツ)などのわかりにくい引数はありませんが、def func(シリアル:CertificateSerialNumber、contents:EncapsulatedContentInfo)、およびPyDERASNはそのような追跡に役立ちますコード。



同時に、PyDERASNにはこの非常にタイピングに非常に便利な譲歩があります。 pyasn1は、SubjectKeyIdentifier()。subtype(implicitTag = Tag(...))によるSubjectKeyIdentifier()オブジェクト(必須のIMPLICIT TAGなし)の割り当てを許可しなかったため、変更されたIMPLICIT / EXPLICITタグのためにオブジェクトをコピーおよび再作成する必要がありました。 PyDERASNは基本タイプのみを厳密に監視します。既存のASN.1構造のタグを自動的に置き換えます。 これにより、アプリケーションコードが大幅に簡素化されます。



デコード中にエラーが発生した場合、pyasn1でエラーが発生した場所を正確に理解することは容易ではありません。 たとえば、すでに言及したトルコ語の証明書では、次のエラーが発生します:UTF8String(tbsCertificate:issuer:rdnSequence:3:0:value:DEFINED BY 2.5.4.10:utf8String)(138)未満足の境界:1⇐77⇐64 ASNを書き込むとき.1構造は人々が間違いを犯す可能性があり、アプリケーションのデバッグや反対側のエンコードされたドキュメントの問題の発見を容易にします。



PyDERASNの最初のバージョンはBERエンコーディングをサポートしていませんでした。 それはずっと後に現れ、タイムゾーンを使用したUTCTime / GeneralizedTime処理はまだサポートされていません。 プロジェクトは主に仕事からの空き時間に書かれているため、これは将来的に行われます。



また、最初のバージョンでは、DEFINED BYフィールドを使用した作業はありませんでした。 数か月後、この機会が現れて積極的に使用され始め、アプリケーションコードを大幅に削減しました。1回のデコード操作で、構造全体を非常に深く分解することができました。 これを行うには、スキームで「決定する」フィールドを設定します。 たとえば、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))), )
      
      





contentTypeがid_signedDataのOIDを含む場合、コンテンツフィールド(同じSEQUENCEにある)はSignedDataスキームを使用してデコードする必要があると言います。 なぜそんなに多くの括弧があるのですか? EnvelopedData構造の場合のように、フィールドは複数のフィールドを同時に「定義」できます。 定義済みフィールドは、いわゆるデコードパスによって識別されます。これは、すべての構造内の要素の正確な位置を設定します。



これらの定義をすぐに回路に導入することは必ずしも望ましいとは限らないか、常に可能であるとは限りません。 OIDと構造がサードパーティのプロジェクトでのみ知られているアプリケーション固有の場合があります。 PyDERASNは、構造のデコード時にこれらの定義を指定する機能を提供します。



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





ここでは、添付されているすべての証明書のCMS SignedDataで、すべての拡張子(AuthorityKeyIdentifier、BasicConstraints、SubjectSignToolなど)をデコードすると言います。 回路内で定義されているかのように、「置換」する要素をデコードパスを通じて示します。



最後に、PyDERASNにはコマンドラインからASN.1ファイルをデコードする機能があり、リッチプリティ印刷があります。 任意のASN.1をデコードするか、明確に定義されたスキームを指定して、次のようなものを見ることができます。



プリティ印刷の例



表示される情報:オブジェクトのオフセット、タグの長さ、長さの長さ、コンテンツの長さ、EOC(オクテットの終わり)の存在、BERエンコードフラグ、エンコードの不定長フラグ、EXPLICITタグの長さとオフセット(存在する場合)、オブジェクトのネストの深さ構造、IMPLICIT / EXPLICITタグ値、スキームによるオブジェクト名、その基本ASN.1タイプ、SEQUENCE / SET OF内のシリアル番号、値CHOICE(存在する場合)、スキームによる人間が読める名前INTEGER / ENUMERATED / BIT STRING、任意の基本タイプの値、サーキットからのDEFAULT / OPTIONALフラグ、オブジェクトがDEFINED BY以降として自動的にデコードされたというサイン OID-、それは起こった、chelovekochitaemyのOIDのGM。



プリティ印刷システムは、別の手段ですでに視覚化されている一連のPPオブジェクトを生成するように特別に作成されています。 スクリーンショットは、レンダラーをプレーンテキストで示しています。 JSON / HTML形式のレンダラーがあり、 asn1jsプロジェクトのようにASN.1ブラウザーで強調表示してこれを見ることができます。



その他の図書館



これは目標ではありませんでしたが、PyDERASNはpyasn1 よりも大幅に高速でした。 たとえば、メガバイトサイズのCRLファイルのデコードには時間がかかり、データストレージの中間形式(高速)を検討し、アプリケーションのアーキテクチャを変更する必要があります。 pyasn1は私のラップトップでCRL CACert.orgを20分以上デコードし、PyDERASNはわずか28秒でデコードします! 暗号化構造の迅速な作業を目的としたasn1cryptoプロジェクトがあります:29秒で同じCRLを(完全に、遅延ではなく)デコードしますが、 Python3で実行するとほぼ2倍のRAMを消費します(983 MiB対498 mi) Python2では3.5倍(488に対して1677)、pyasn1は4.3倍(488に対して2093)を消費します。



私が言及したasn1cryptoは、プロジェクトがまだ始まったばかりであり、それについて聞いていなかったため、考慮しませんでした。 彼は同じGeneralizedTimeを使用しないことがすぐにわかったので、彼らは彼の方向も見ませんでした。シリアル化中に、彼は静かに1秒未満を削除しました。 これはX.509証明書での作業には許容されますが、一般的には機能しません。



現時点では、PyDERASNは私が知っている無料のPython / Go DERデコーダーの中で最も厳密です。 私のお気に入りのGoのエンコーディング/ asn1ライブラリには、OBJECT IDENTIFIERおよびUTCTime / GeneralizedTime文字列の厳密なチェックはありません 。 厳密性が干渉する場合があります(最初は、誰も修正しない古いアプリケーションとの下位互換性のため)。そのため、デコード中のPyDERASNでは、 さまざまな設定を弱めるチェックに合格できます。



プロジェクトコードは可能な限りシンプルにしようとします。 ライブラリ全体は単一のファイルです。 コードは、不必要なパフォーマンスやDRYコードの最適化なしに、理解しやすさに重点を置いて記述されています。 既に述べたように、UTCTime / GeneralizedTime文字列、およびREAL、RELATIVE OID、EXTERNAL、INSTANCE OF、EMBEDDED PDV、CHARACTER STRINGデータ型の本格的なBERデコードはサポートしていません。 他のすべての場合、個人的には、Pythonで他のライブラリを使用する理由はありません。



PyGOSTGoGOSTNNCPGoVPNなどの私のすべてのプロジェクトと同様に、PyDERASNはLGPLv3 +の条件の下で配布される完全に無料のソフトウェアであり、無料でダウンロードできます。 使用例はPyGOSTテストにあります。



Sergey Matveev暗号銀行Open Society Foundation Foundationのメンバー、Python / Go-developer、 FSUE「STC Atlas」のチーフスペシャリスト。



All Articles