ASN.1とDERへの温かい歓迎

デンマーク語で見る

フランス語で見る

ヘブライ語に切り替える

日本語で表示する

ウクライナ語で表示する

簡体字中国語のページを読む

このドキュメントは、HTTPSで使用される証明書を定義するデータ構造と形式の概要を分かりやすく説明しています。少しのコンピューターサイエンスの経験と証明書についての少しの知識があれば、誰でも理解できるはずです。

HTTPS証明書は、他のファイルと同様に、一種のファイルです。その内容は、RFC 5280で定義された形式に従っています。定義はASN.1で記述されており、これはファイル形式(または同等のデータ構造)を定義するために使用される言語です。たとえば、Cでは次のように記述するかもしれません。

struct point {
  int x, y;
  char label[10];
};

Goでは次のように記述します。

type point struct {
  x, y int
  label string
}

そしてASN.1では次のように記述します。

Point ::= SEQUENCE {
  x INTEGER,
  y INTEGER,
  label UTF8String
}

GoやCの定義ではなくASN.1定義を書く利点は、言語に依存しないことです。PointのASN.1定義はどの言語でも実装できます。または(できれば)、ASN.1定義を受け取り、お好みの言語で実装するコードを自動的に生成するツールを使用できます。ASN.1定義のセットを「モジュール」と呼びます。

ASN.1のもう1つの重要な点は、さまざまなシリアル化形式(メモリ内のデータ構造をバイトのシーケンス(またはファイル)に変換し、再び元に戻す方法)を備えていることです。これにより、あるマシンで生成された証明書を、異なるCPUとオペレーティングシステムを使用している別のマシンでも読み取ることができます。

ASN.1と同じことをする他の言語もあります。たとえば、Protocol Buffersは、型の定義のための言語と、定義した型のオブジェクトをエンコードするためのシリアル化形式の両方を提供しています。Thriftも言語とシリアル化形式の両方を持っています。Protocol BuffersまたはThriftのいずれかが、HTTPS証明書の形式を定義するために使用されていてもよかったのですが、ASN.1(1984年)は、証明書(1988年)とHTTPS(1994年)が発明された時点で既に存在していたという大きな利点がありました。

ASN.1は長年にわたって何度も改訂されており、通常は発行された年で版が識別されます。このドキュメントは、RFC 5280およびHTTPS証明書に関連する他の標準を明確に理解するために十分なASN.1を教えることを目的としているため、主に1988年版について説明し、以降の版に追加された機能についてはいくつかの注記を付けます。さまざまな版はITUから直接ダウンロードできますが、一部はITU会員のみが利用できることに注意してください。関連する標準はX.680(ASN.1言語を定義)とX.690(シリアル化形式DERとBERを定義)です。これらの標準の以前のバージョンは、それぞれX.208X.209でした。

ASN.1の主なシリアル化形式は「Distinguished Encoding Rules」(DER)です。これらは、正規化が追加された「Basic Encoding Rules」(BER)のバリアントです。たとえば、型にSET OFが含まれている場合、DERシリアル化のためにメンバーをソートする必要があります。

DERで表現された証明書は、多くの場合、さらにPEMにエンコードされます。PEMはbase64を使用して任意のバイトを英数字(および「+」と「/」)としてエンコードし、区切り行( "-----BEGIN CERTIFICATE-----"と "-----END CERTIFICATE-----")を追加します。PEMは、コピーアンドペーストが容易なため便利です。

このドキュメントでは、最初にASN.1で使用される型と表記法について説明し、次にASN.1を使用して定義されたオブジェクトのエンコード方法について説明します。ASN.1言語のいくつかの機能がエンコードの詳細を直接指定しているため、セクション間を自由に前後に移動してください。このドキュメントでは、より一般的な用語を優先し、「オクテット」の代わりに「バイト」、「内容」の代わりに「値」を使用しています。「シリアル化」と「エンコーディング」は互換的に使用しています。

整数

古き良きおなじみの整数です。正または負にすることができます。ASN.1の整数で本当に珍しいのは、任意の大きさにすることができることです。int64に十分な容量がありませんか?問題ありません。これは、int64よりもはるかに大きいRSAモジュール(22048など)を表すのに特に便利です。技術的にはDERには最大整数が存在しますが、非常に大きいです。任意のDERフィールドの長さは、最大126バイトのシリーズとして表現できます。したがって、DERで表現できる最大の整数は256(2**1008)-1です。真に無制限の整数の場合、無限長のフィールドを許可するBERでエンコードする必要があります。

文字列

ASN.1には多くの文字列型があります:BMPString、GeneralString、GraphicString、IA5String、ISO646String、NumericString、PrintableString、TeletexString、T61String、UniversalString、UTF8String、VideotexString、VisibleString。HTTPS証明書では、主にPrintableString、UTF8String、およびIA5Stringを扱う必要があります。特定のフィールドの文字列型は、そのフィールドを定義するASN.1モジュールによって定義されます。たとえば

CPSuri ::= IA5String

PrintableStringはASCIIの制限されたサブセットであり、英数字、スペース、および特定の少数の句読点:' () + , - . / : = ?を許可しています。特に*@は含まれていません。より制限された文字列型には、ストレージサイズの利点はありません。

一部のフィールド(RFC 5280のDirectoryStringなど)では、シリアル化コードが複数の文字列型から選択できます。DERエンコーディングには使用している文字列の型が含まれているため、PrintableStringとしてエンコードするものは実際にPrintableStringの要件を満たしていることを確認してください。

国際アルファベット第5号に基づいたIA5Stringはより寛容です。ほぼすべてのASCII文字を許可し、証明書内の電子メールアドレス、DNS名、およびURLに使用されます。バイト値のIA5の意味が、同じ値のUS-ASCIIの意味とは異なるバイト値がいくつかあることに注意してください。

TeletexString、BMPString、およびUniversalStringはHTTPS証明書での使用は非推奨ですが、長寿命で廃止前に作成された古いCA証明書を解析するときに表示される場合があります。

ASN.1の文字列は、CとC++の文字列のようにヌル終端されていません。実際、埋め込まれたヌルバイトを持つことは完全に合法です。これは、2つのシステムが同じASN.1文字列を異なる解釈すると、脆弱性を引き起こす可能性があります。たとえば、一部のCAは以前、「example.com\0.evil.com」に対してevil.comの所有権に基づいて発行をだませました。当時の証明書検証ライブラリは、結果を「example.com」に対して有効なものとして扱っていました。脆弱性を回避するために、CとC++でASN.1文字列を処理する際には十分に注意してください。

日付と時刻

ここでも、多くの時刻型があります:UTCTime、GeneralizedTime、DATE、TIME-OF-DAY、DATE-TIME、DURATION。HTTPS証明書では、UTCTimeとGeneralizedTimeだけを扱う必要があります。

UTCTimeは、日付と時刻をYYMMDDhhmm[ss]として表し、オプションのタイムゾーンオフセットまたは「Z」を使用してZulu(別名UTC、別名0タイムゾーンオフセット)を表します。たとえば、UTCTimeの820102120000Zと820102070000-0500はどちらも同じ時刻を表しています。1982年1月2日、ニューヨーク市(UTC-5)の午前7時とUTCの正午です。

UTCTimeは1900年代か2000年代か不明確であるため、RFC 5280は、1950年から2050年の日付を表すことを明確にしています。RFC 5280は、「Z」タイムゾーンを使用する必要があり、秒を含める必要があることも要求しています。

GeneralizedTimeは、年を4桁で表すことで2050年以降の日付をサポートしています。また、小数秒(奇妙なことに、コンマまたはピリオドを小数点セパレータとして使用)も許可しています。RFC 5280は、小数秒を禁止し、「Z」を要求しています。

オブジェクト識別子

オブジェクト識別子は、一連の整数で構成されるグローバルに一意の階層型識別子です。「もの」のあらゆる種類を参照できますが、一般的に標準、アルゴリズム、証明書拡張、組織、またはポリシードキュメントを識別するために使用されます。例として:1.2.840.113549はRSA Security LLCを識別します。RSAは、1.2.840.113549.1.1.11のように、そのプレフィックスで始まるOIDを割り当てることができ、これはRFC 8017で定義されているsha256WithRSAEncryptionを識別します。

同様に、1.3.6.1.4.1.11129はGoogle, Inc.を識別します。Googleは1.3.6.1.4.1.11129.2.4.2Certificate Transparencyで使用されるSCTリスト拡張(当初はGoogleで開発された)を識別するために割り当てており、RFC 6962で定義されています。

特定のプレフィックスの下に存在できる子OIDのセットを「OIDアーク」と呼びます。より短いOIDの表現は小さいので、より短いアークの下のOIDの割り当ては、特にそのOIDを多く送信する必要がある形式では、より価値があると見なされます。OIDアーク2.5は「ディレクトリサービス」に割り当てられており、HTTPS証明書が基づいているX.509を含む仕様のシリーズです。証明書の多くのフィールドは、その便利な短いアークで始まります。たとえば、2.5.4.6は「countryName」を意味し、2.5.4.10は「organizationName」を意味します。ほとんどの証明書では、これらのOIDのそれぞれを少なくとも1回エンコードする必要があるため、それらが短いのは便利です。

仕様のOIDは、便宜上、人間が読み取れる名前で表されることが多く、別のOIDとの連結によって指定される場合があります。RFC 8017からの例

   pkcs-1    OBJECT IDENTIFIER ::= {
       iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1
   }
   ...

   sha256WithRSAEncryption      OBJECT IDENTIFIER ::= { pkcs-1 11 }

NULL

NULLは単なるNULLです。

SEQUENCEとSEQUENCE OF

名前でだまされないでください。これらは2つの非常に異なる型です。SEQUENCEは、ほとんどのプログラミング言語における「struct」に相当します。異なる型の固定数のフィールドを保持します。たとえば、下の証明書の例を参照してください。

一方、SEQUENCE OFは、単一型の任意の数のフィールドを保持します。これは、プログラミング言語における配列またはリストと同様です。たとえば

   RDNSequence ::= SEQUENCE OF RelativeDistinguishedName

これは、特定の順序で0、1、または7,000個のRelativeDistinguishedNamesにすることができます。

SEQUENCEとSEQUENCE OFには、1つの類似点があります。どちらも同じ方法でエンコードされます!エンコーディングセクションで詳しく説明します。

SETとSET OF

これらはSEQUENCEとSEQUENCE OFとほぼ同じですが、要素の順序には意図的にセマンティクスがありません。ただし、エンコード形式ではソートする必要があります。

RelativeDistinguishedName ::=
  SET SIZE (1..MAX) OF AttributeTypeAndValue

注記: この例では、SIZEキーワードを使用して、RelativeDistinguishedNameに少なくとも1つのメンバーが含まれていることをさらに指定していますが、一般的にSETまたはSET OFはサイズ0を持つことができます。

BIT STRINGとOCTET STRING

これらはそれぞれ任意のビットまたはバイトを含みます。これらは、nonceやハッシュ関数の出力など、非構造化データの保持に使用できます。また、CのvoidポインタやGoの空インターフェース型(interface{})のように使用することもできます。これは、構造化データを持つデータですが、その構造は型システムとは別に理解または定義されている場合の方法です。例えば、証明書の署名はBIT STRINGとして定義されています。

Certificate  ::=  SEQUENCE  {
     tbsCertificate       TBSCertificate,
     signatureAlgorithm   AlgorithmIdentifier,
     signature            BIT STRING  }

ASN.1言語の新しいバージョンではBIT STRING内(およびOCTET STRINGについても同様)の内容をより詳細に指定できます。

CHOICEとANY

CHOICEは、その定義にリストされている型のうち正確に1つを含むことができる型です。例えば、TimeはUTCTimeまたはGeneralizedTimeの正確に1つを含むことができます。

Time ::= CHOICE {
     utcTime        UTCTime,
     generalTime    GeneralizedTime }

ANYは、値が任意の型であることを示します。実際には、ASN.1文法では完全に表現できないものによって制限されることが一般的です。例えば

   AttributeTypeAndValue ::= SEQUENCE {
     type     AttributeType,
     value    AttributeValue }

   AttributeType ::= OBJECT IDENTIFIER

   AttributeValue ::= ANY -- DEFINED BY AttributeType

これは、主要な仕様が公開された後に追加フィールドを個別に定義するための余地を残したい拡張機能に特に役立ちます。そのため、新しい型(オブジェクト識別子)を登録し、それらの型の定義で新しいフィールドの構造を指定する方法があります。

ANYは1988年のASN.1表記の遺物であることに注意してください。1994年版では、ANYは非推奨となり、Information Object Classesに置き換えられました。これは、人々がANYから望んでいた拡張機能の動作の種類を指定するための高度で形式化された方法です。この変更は現在では非常に古いため、最新のASN.1仕様(2015年)ではANYについて言及されていません。しかし、1994年版を見ると、切り替えに関する議論を見ることができます。RFC 5280がまだ使用しているため、ここで古い構文を含めています。RFC 5912は、2002年のASN.1構文を使用して、RFC 5280およびいくつかの関連仕様からの同じ型を表しています。

その他の表記

コメントは--で始まります。SEQUENCEまたはSETのフィールドはOPTIONALとマークすることも、DEFAULT fooとマークすることもできます。これはOPTIONALと同じ意味ですが、フィールドがない場合、「foo」を含むものと見なされることを意味します。長さを持つ型(文字列、オクテットとビット文字列、セットとシーケンスOFもの)には、長さを正確な長さまたは範囲に制限するSIZEパラメータを与えることができます。

型は、型定義の後に波括弧を使用して、特定の値を持つように制約することができます。この例では、Versionフィールドが3つの値を持つことができ、それらの値に意味のある名前を割り当てています。

Version ::= INTEGER { v1(0), v2(1), v3(2) }

これは、特定のOIDに名前を割り当てる際にもよく使用されます(これは代替値を示すカンマのない単一の値であることに注意してください)。RFC 5280からの例

id-pkix  OBJECT IDENTIFIER  ::=
         { iso(1) identified-organization(3) dod(6) internet(1)
                    security(5) mechanisms(5) pkix(7) }

[number]、IMPLICIT、EXPLICIT、UNIVERSAL、およびAPPLICATIONも表示されます。これらは、値をどのようにエンコードするかの詳細を定義しており、後で説明します。

エンコーディング

ASN.1は、BER、DER、PER、XERなど、多くのエンコーディングに関連付けられています。基本エンコーディングルール(BER)は非常に柔軟です。Distinguished Encoding Rules(DER)は、正規化ルールを持つBERのサブセットであるため、特定の構造を表す方法は1つだけです。Packed Encoding Rules(PER)は、エンコードする際に使用するバイト数が少ないため、スペースまたは伝送時間が重要な場合に役立ちます。XML Encoding Rules(XER)は、何らかの理由でXMLを使用したい場合に役立ちます。

HTTPS証明書は一般的にDERでエンコードされます。BERでエンコードすることは可能ですが、署名値は証明書内の正確なバイトではなく、同等のDERエンコーディングに対して計算されるため、証明書をBERでエンコードすると、不必要な問題が発生します。BERについて説明し、DERによって提供される追加の制限について説明します。

別のウィンドウで開いている実際の証明書のデコードを別のウィンドウで開いて、このセクションを読むことをお勧めします。

型-長さ-値

BERは、Protocol BuffersやThriftと同様に、型-長さ-値エンコーディングです。つまり、BERでエンコードされたバイトを読み取ると、最初にASN.1ではタグと呼ばれる型に遭遇します。これは、エンコードされたものの型(INTEGER、UTF8String、構造体など)を示すバイトまたはバイトのシーケンスです。

長さ
02 03 01 00 01

次に、長さに遭遇します。これは、値を取得するために読み取る必要があるデータバイト数を示す数値です。そしてもちろん、値自体を含むバイトが続きます。例として、16進バイト02 03 01 00 01は、INTEGER(タグ02はINTEGER型に対応)、長さ03、および01 00 01で構成される3バイトの値を表します。

型-長さ-値は、JSON、CSV、またはXMLなどの区切り付きエンコーディングとは異なります。フィールドの長さを事前に知るのではなく、予想される区切り文字(例:JSONの}、XMLの</some-tag>)に当たるまでバイトを読み取ります。

タグ

タグは通常1バイトです。複数のバイトを使用して任意の大きなタグ番号をエンコードする手段(「高いタグ番号」形式)がありますが、これは通常必要ありません。

いくつかのタグの例を以下に示します。

タグ(10進数) タグ(16進数)
2 02 整数
3 03 BIT STRING
4 04 OCTET STRING
5 05 NULL
6 06 オブジェクト識別子
12 0C UTF8String
16 10(および30)* SEQUENCEとSEQUENCE OF
17 11(および31)* SETとSET OF
19 13 PrintableString
22 16 IA5String
23 17 UTCTime
24 18 GeneralizedTime

これらと、退屈なのでスキップした他のいくつかは、「ユニバーサル」タグです。これは、コアASN.1仕様で指定されており、すべてのASN.1モジュールで同じ意味を持つためです。

これらのタグはすべて31(0x1F)未満であり、それは良い理由があります。ビット8、7、および6(タグバイトの上位ビット)は追加情報をエンコードするために使用されるため、31より大きいユニバーサルタグ番号は、「高いタグ番号」形式を使用する必要があり、追加のバイトが必要になります。31より大きいユニバーサルタグは少数ありますが、非常にまれです。

*でマークされた2つのタグは、常に0x30または0x31としてエンコードされます。これは、ビット6がフィールドが構築済みかプリミティブかを指示するために使用されるためです。これらのタグは常に構築済みであるため、そのエンコーディングではビット6が1に設定されています。構築済みとプリミティブセクションで詳細を参照してください。

タグクラス

ユニバーサルクラスですべての「良い」タグ番号が使用されたからといって、独自のタグを定義できなくなるわけではありません。「アプリケーション」、「プライベート」、および「コンテキスト固有」のクラスもあります。これらはビット8と7によって区別されます。

クラス ビット8 ビット7
ユニバーサル 0 0
アプリケーション 0 1
コンテキスト固有 1 0
プライベート 1 1

仕様では、最も重要なビルディングブロックを提供するため、主にユニバーサルクラスのタグを使用します。たとえば、証明書のシリアル番号は、プレーンなINTEGER(タグ番号0x02)でエンコードされます。しかし、仕様では、存在しない場合にエンコーディングから完全に省略されるOPTIONALフィールドを区別したり、同じ型を持つ複数のエントリを持つCHOICEを区別したりするために、コンテキスト固有のクラスでタグを定義する必要がある場合があります。たとえば、この定義を見てください。

Point ::= SEQUENCE {
  x INTEGER OPTIONAL,
  y INTEGER OPTIONAL
}

OPTIONALフィールドは、存在しない場合にエンコーディングから完全に省略されるため、x座標のみを持つPointとy座標のみを持つPointを区別することが不可能になります。たとえば、x座標が9のPointは次のようにエンコードします(ここでは30はSEQUENCEを意味します)。

30 03 02 01 09

これは、長さ3(バイト)のSEQUENCEで、長さ1のINTEGER(値は9)を含んでいます。しかし、y座標が9のPointもまったく同じ方法でエンコードされるため、曖昧性があります。

エンコーディング命令

この曖昧さを解決するために、仕様では各エントリに一意のタグを割り当てるエンコーディング命令を提供する必要があります。ユニバーサルタグを踏み台にすることは許可されていないため、他のタグを使用する必要があります。たとえば、APPLICATIONです。

Point ::= SEQUENCE {
  x [APPLICATION 0] INTEGER OPTIONAL,
  y [APPLICATION 1] INTEGER OPTIONAL
}

ただし、このユースケースでは、実際にはコンテキスト固有のクラスを使用する方がはるかに一般的です。これは、単独でブラケット内の数値で表されます。

Point ::= SEQUENCE {
  x [0] INTEGER OPTIONAL,
  y [1] INTEGER OPTIONAL
}

そのため、x座標が9だけのPointをエンコードするには、xをUNIVERSAL INTEGERとしてエンコードする代わりに、エンコードされたタグのビット8と7を(1, 0)に設定してコンテキスト固有のクラスを示し、下位ビットを0に設定して、次のエンコーディングを作成します。

30 03 80 01 09

y座標が9だけのPointを表すには、下位ビットを1に設定する以外は同じことを行います。

30 03 81 01 09

または、x座標とy座標が両方とも9のPointを表すこともできます。

30 06 80 01 09 81 01 09

長さ

タグ-長さ-値タプル内の長さは、常にすべてのサブオブジェクトを含むオブジェクトの総バイト数を表します。そのため、1つのフィールドを持つSEQUENCEの長さは1ではありません。エンコードされた形式のフィールドが占めるバイト数です。

長さのエンコーディングには、短い形式と長い形式の2つの形式があります。短い形式は、0〜127の単一バイトです。

長い形式は少なくとも2バイトの長さで、最初のバイトのビット8が1に設定されています。最初のバイトのビット7-1は、長さフィールド自体にさらに何バイトあるかを示します。残りのバイトは、長さ自体を複数バイトの整数として指定します。

ご想像のとおり、これにより非常に長い値をエンコードできます。最長の可能な長さはバイト254で始まり(長さバイト255は将来の拡張用に予約されています)、長さフィールド自体にさらに126バイトがあることを示します。それらの126バイトのそれぞれが255の場合、値フィールドに続くバイト数は21008-1になります。

長い形式を使用すると、同じ長さを複数の方法でエンコードできます。たとえば、1つに収まる長さを2バイトで表現したり、短い形式に収まる長さを長い形式で表現したりできます。DERでは、常に最小限の長さ表現を使用するように指示されています。

安全に関する警告:デコードする長さの値を完全に信頼しないでください!たとえば、エンコードされた長さが、デコードされているストリームから利用可能なデータ量よりも少ないことを確認してください。

不定長

BERでは、事前に長さが分からない文字列、SEQUENCE、SEQUENCE OF、SET、またはSET OFをエンコードすることも可能です(例えば、ストリーミング出力時など)。これを行うには、長さを値が80の1バイトでエンコードし、値を連結されたエンコードオブジェクトのシリーズとしてエンコードします。終端は00 00という2バイトで示されます(タグ0を持つ長さ0のオブジェクトと見なすことができます)。例えば、UTF8Stringの不定長エンコーディングは、1つ以上のUTF8Stringを連結し、最後に00 00を連結したエンコーディングになります。

不定性は任意にネストすることができます!例えば、不定長のUTF8Stringを形成するために連結するUTF8Stringは、それぞれ定長または不定長のどちらかでエンコードできます。

長さバイトの80は、有効な短形式または長形式の長さではないため、区別されます。ビット8が1に設定されているため、これは通常長形式として解釈されますが、残りのビットは長さを構成する追加バイト数を示す必要があります。ビット7〜1がすべて0であるため、長さを作るバイト数が0の長形式エンコーディングを示すことになりますが、これは許可されていません。

DERは不定長エンコーディングを禁止しています。定長エンコーディング(つまり、先頭に長さを指定したもの)を使用する必要があります。

構成済み vs プリミティブ

最初のタグバイトのビット6は、値がプリミティブ形式でエンコードされているか、構成済み形式でエンコードされているかを示すために使用されます。プリミティブエンコーディングは値を直接表現します。例えば、UTF8Stringでは、値はUTF-8バイトの文字列自体のみで構成されます。構成済みエンコーディングは、値を他のエンコードされた値の連結として表現します。「不定長」セクションで説明したように、構成済みエンコーディングのUTF8Stringは、複数のエンコードされたUTF8String(それぞれタグと長さを持つ)を連結して構成されます。全体的なUTF8Stringの長さは、連結されたすべてのエンコードされた値の総バイト数になります。構成済みエンコーディングは、定長または不定長のどちらかを使用できます。プリミティブエンコーディングは常に定長を使用します。構成済みエンコーディングを使用せずに不定長を表現する方法がないためです。

INTEGER、OBJECT IDENTIFIER、およびNULLは、プリミティブエンコーディングを使用する必要があります。SEQUENCE、SEQUENCE OF、SET、およびSET OFは、(複数の値の連結であるため)構成済みエンコーディングを使用する必要があります。BIT STRING、OCTET STRING、UTCTime、GeneralizedTime、およびさまざまな文字列型は、BERでは送信者の裁量でプリミティブエンコーディングまたは構成済みエンコーディングのどちらかを使用できます。ただし、DERでは、プリミティブと構成済みのどちらかのエンコーディングを選択できるすべての型で、プリミティブエンコーディングを使用する必要があります。

明示的 vs 暗黙的

上記で説明されているエンコーディング命令(例:[1]または[APPLICATION 8])には、キーワードEXPLICITまたはIMPLICITを含めることもできます(RFC 5280からの例)。

TBSCertificate  ::=  SEQUENCE  {
     version         [0]  Version DEFAULT v1,
     serialNumber         CertificateSerialNumber,
     signature            AlgorithmIdentifier,
     issuer               Name,
     validity             Validity,
     subject              Name,
     subjectPublicKeyInfo SubjectPublicKeyInfo,
     issuerUniqueID  [1]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                          -- If present, version MUST be v2 or v3
     extensions      [3]  Extensions OPTIONAL
                          -- If present, version MUST be v3 --  }

これは、タグをどのようにエンコードすべきかを定義するものであり、タグ番号が明示的に割り当てられているかどうかに関係ありません(IMPLICITとEXPLICITの両方が常に特定のタグ番号と共に使用されるため)。IMPLICITは、ASN.1モジュールで提供されたタグ番号とクラスを使用して、基礎となる型と同じようにフィールドをエンコードします。EXPLICITは、フィールドを基礎となる型としてエンコードしてから、それを外部エンコーディングでラップします。外部エンコーディングには、ASN.1モジュールからのタグ番号とクラスがあり、さらに構成済みビットが設定されています。

IMPLICITを使用したASN.1エンコーディング命令の例を次に示します。

[5] IMPLICIT UTF8String

これは「hi」を次のようにエンコードします。

85 02 68 69

EXPLICITを使用したこのASN.1エンコーディング命令と比較してください。

[5] EXPLICIT UTF8String

これは「hi」を次のようにエンコードします。

A5 04 0C 02 68 69

IMPLICITまたはEXPLICITキーワードが存在しない場合、モジュールが最上位で「EXPLICIT TAGS」、「IMPLICIT TAGS」、または「AUTOMATIC TAGS」を使用して異なるデフォルトを設定しない限り、デフォルトはEXPLICITになります。例えば、RFC 5280は2つのモジュールを定義しています。1つはEXPLICITタグがデフォルトであり、もう1つは最初のモジュールをインポートし、IMPLICITタグをデフォルトとしています。暗黙的エンコーディングは、明示的エンコーディングよりもバイト数が少なくなります。

AUTOMATIC TAGSはIMPLICIT TAGSと同じですが、オプションフィールドを持つSEQUENCEなど、必要な場所にタグ番号([0][1]など)が自動的に割り当てられるという追加の特性があります。

特定の型のエンコーディング

このセクションでは、例を挙げて各型の値のエンコード方法について説明します。

INTEGERエンコーディング

整数 は、最左バイトの最上位ビット(ビット8)を符号ビットとして、2の補数で1バイト以上でエンコードされます。BER仕様にあるように

2の補数バイナリ数の値は、最後のオクテットのビット1をビット0として開始し、最初のオクテットのビット8で番号付けを終了して、内容オクテットのビットを番号付けすることによって導き出されます。各ビットには、2Nの数値が割り当てられます。ここで、Nはその数値シーケンスにおける位置です。2の補数バイナリ数の値は、1に設定されているビットに対して割り当てられた数値の合計を求め、最初のオクテットのビット8が1に設定されている場合は、そのビットに割り当てられた数値を減算することによって得られます。

例えば、この1バイトの値(バイナリで表現)は10進数の50をエンコードします。

00110010 (== 10進数50)

この1バイトの値(バイナリで表現)は10進数の-100をエンコードします。

10011100 (== 10進数-100)

この5バイトの値(バイナリで表現)は、10進数-549755813887(つまり、-239 + 1)をエンコードします。

10000000 00000000 00000000 00000000 00000001 (== 10進数-549755813887)

BERとDERの両方で、整数は可能な限り最短形式で表現する必要があります。これは次の規則で適用されます。

... the bits of the first octet and bit 8 of the second octet:

1.  shall not all be ones; and
2.  shall not all be zero.

規則(2)はおおよそ次のことを意味します。エンコーディングに先頭のゼロバイトがある場合、それらを省略しても同じ数値になります。2番目のバイトのビット8もここで重要です。特定の値を表すには、先頭のゼロバイトを使用する必要があるためです。例えば、10進数の255は2バイトでエンコードされます。

00000000 11111111

これは、11111111の1バイトエンコーディングだけでは-1(ビット8は符号ビットとして扱われます)を意味するためです。

規則(1)は、例で説明するのが最適です。10進数の-128は次のようにエンコードされます。

10000000 (== 10進数-128)

ただし、これは次のようにエンコードすることもできます。

11111111 10000000 (== 10進数-128、ただし無効なエンコーディング)

展開すると、-215 + 214 + 213 + 212 + 211 + 210 + 29 + 28 + 27 == -27 == -128となります。「10000000」の1は、1バイトエンコーディングでは符号ビットでしたが、2バイトエンコーディングでは27を意味します。

これは一般的な変換です。BER(またはDER)としてエンコードされた負の数には、11111111を接頭辞として付けることで同じ数値を取得できます。これは符号拡張と呼ばれます。または同等に、値のエンコーディングが11111111で始まる負の数がある場合、そのバイトを削除しても同じ数値になります。したがって、BERとDERは最短のエンコーディングを要求します。

INTEGERの2の補数エンコーディングは、証明書発行における実際的な影響があります。RFC 5280では、シリアル番号を正の数にする必要があります。最初のビットは常に符号ビットであるため、DERで8バイトとしてエンコードされたシリアル番号は、最大63ビットの長さになります。64ビットの正のシリアル番号をエンコードするには、9バイトのエンコードされた値(最初のバイトは0)が必要です。

263+1(これは64ビットの正の数です)の値を持つINTEGERのエンコーディングを次に示します。

02 09 00 80 00 00 00 00 00 00 01

文字列エンコーディング

文字列は、リテラルバイトとしてエンコードされます。IA5StringとPrintableStringは、許容される文字の異なるサブセットを定義するだけなので、それらのエンコーディングはタグのみが異なります。

「hi」を含むPrintableString

13 02 68 69

「hi」を含むIA5String

16 02 68 69

UTF8Stringも同じですが、より幅広い種類の文字をエンコードできます。例えば、これはU+1F60Eサングラスをかけた笑顔(😎)を含むUTF8Stringのエンコーディングです。

0c 04 f0 9f 98 8e

日付と時刻のエンコーディング

驚くべきことに、UTCTimeとGeneralizedTimeは実際には文字列のようにエンコードされます!「型」セクションで上記のように説明されているように、UTCTimeはYYMMDDhhmmss形式で日付を表します。GeneralizedTimeは、YYの代わりに4桁の年YYYYを使用します。どちらも、UTCからのタイムゾーンオフセットがないことを示すオプションのタイムゾーンオフセットまたは「Z」(Zulu)があります。

例えば、PSTタイムゾーン(UTC-8)の2019年12月15日19:02:10は、UTCTimeでは191215190210-0800として表されます。BERでエンコードすると、次のようになります。

17 11 31 39 31 32 31 35 31 39 30 32 31 30 2d 30 38 30 30

BERエンコーディングでは、UTCTimeとGeneralizedTimeの両方で秒はオプションであり、タイムゾーンオフセットが許可されています。ただし、DER(RFC 5280を含む)では、秒を含める必要があり、秒の小数部は含めることができず、「Z」形式でUTCとして表現する必要があります。

上記の日付は、DERでは次のようにエンコードされます。

17 0d 31 39 31 32 31 36 30 33 30 32 31 30 5a

OBJECT IDENTIFIERエンコーディング

上記で説明したように、OIDは概念的には一連の整数です。常に少なくとも2つのコンポーネントで構成されます。最初のコンポーネントは常に0、1、または2です。最初のコンポーネントが0または1の場合、2番目のコンポーネントは常に40未満です。このため、最初の2つのコンポーネントは、Xが最初のコンポーネントでYが2番目のコンポーネントである40*X+Yとして一意に表されます。

したがって、例えば、2.999.3をエンコードするには、最初の2つのコンポーネントを10進数の1079(40*2 + 999)に結合し、「1079.3」になります。

この変換を適用した後、各コンポーネントは、最上位バイトを先頭に128進数でエンコードされます。ビット8は、コンポーネントの最後のバイトを除くすべてのバイトで「1」に設定されます。これにより、1つのコンポーネントが終了し、次のコンポーネントが始まることが分かります。「3」というコンポーネントは、単にバイト0x03として表されます。「129」というコンポーネントは、バイト0x81 0x01として表されます。エンコードされると、OIDのすべてのコンポーネントは連結されて、OIDのエンコードされた値を形成します。

OIDは、BERでもDERでも、可能な限り最少のバイト数で表現する必要があります。したがって、コンポーネントはバイト0x80で開始することはできません。

例として、OID 1.2.840.113549.1.1.11(sha256WithRSAEncryptionを表す)は、次のようにエンコードされます。

06 09 2a 86 48 86 f7 0d 01 01 0b

NULLエンコーディング

NULLを含むオブジェクトの値は常に長さ0であるため、NULLのエンコーディングは常にタグと長さフィールド0だけです。

05 00

SEQUENCEエンコーディング

SEQUENCEについて最初に知っておくべきことは、他のオブジェクトを含むため、常に構成済みエンコーディングを使用することです。つまり、SEQUENCEの値バイトには、そのSEQUENCEのエンコードされたフィールド(フィールドが定義された順序で)の連結が含まれます。これはまた、SEQUENCEのタグのビット6(構成済み vs プリミティブビット)が常に1に設定されていることも意味します。したがって、SEQUENCEのタグ番号は技術的には0x10ですが、エンコードされたタグバイトは常に0x30になります。

OPTIONALアノテーションが付いたフィールドがSEQUENCEにある場合、存在しない場合はエンコーディングから単純に省略されます。デコーダは、SEQUENCEの要素を処理するとき、これまでデコードされたものと、読み取ったタグバイトに基づいて、どの型がデコードされているかを判断できます。要素が同じ型である場合など、あいまいさがある場合、ASN.1モジュールは、要素に異なるタグ番号を割り当てるエンコーディング命令を指定する必要があります。

DEFAULTフィールドはOPTIONALフィールドに似ています。フィールドの値がデフォルトの場合、BERエンコーディングから省略できます。DERエンコーディングでは、省略する必要があります。

例として、RFC 5280はAlgorithmIdentifierをSEQUENCEとして定義しています。

   AlgorithmIdentifier  ::=  SEQUENCE  {
        algorithm               OBJECT IDENTIFIER,
        parameters              ANY DEFINED BY algorithm OPTIONAL  }

AlgorithmIdentifierのエンコーディング(1.2.840.113549.1.1.11を含む)を示します。RFC 8017では“パラメータ”はこのアルゴリズムに対してNULL型であるべきだと述べられています

30 0d 06 09 2a 86 48 86 f7 0d 01 01 0b 05 00

SEQUENCE OFエンコーディング

SEQUENCE OFは、SEQUENCEと全く同じ方法でエンコードされます。タグも同一です!デコードする場合、SEQUENCEとSEQUENCE OFの違いを判別できるのは、ASN.1モジュールを参照した場合のみです。

数字7、8、9を含むSEQUENCE OF INTEGERのエンコーディングを示します。

30 09 02 01 07 02 01 08 02 01 09

SETエンコーディング

SEQUENCEと同様に、SETは構築型であり、その値のバイトはエンコードされたフィールドの連結です。タグ番号は0x11です。構築型と基本型ビット(ビット6)は常に1に設定されているため、タグバイト0x31でエンコードされます。

SEQUENCEと同様に、SETのエンコーディングでは、OPTIONALフィールドとDEFAULTフィールドは、存在しない場合、またはデフォルト値を持つ場合は省略されます。同じ型を持つフィールドによって生じる曖昧さは、ASN.1モジュールによって解決する必要があり、DEFAULTフィールドは、デフォルト値を持つ場合はDERエンコーディングから省略されなければなりません。

BERでは、SETは任意の順序でエンコードできます。DERでは、SETは各要素のシリアル化された値で昇順にエンコードする必要があります。

SET OFエンコーディング

SET OFアイテムは、タグバイト0x31を含む、SETと同じ方法でエンコードされます。DERエンコーディングの場合、SET OFも昇順でエンコードするという同様の要件があります。SET OFのすべての要素は同じ型であるため、タグによる順序付けでは不十分です。そのため、SET OFの要素は、エンコードされた値でソートされ、短い値は右側にゼロで埋められたものとして扱われます。

BIT STRINGエンコーディング

NビットのBIT STRINGは、N/8バイト(切り上げ)としてエンコードされ、ビット数が8の倍数でない場合の明確化のために、「未使用ビット数」を含む1バイトのプレフィックスが付きます。たとえば、ビット文字列011011100101110111(18ビット)をエンコードする場合、少なくとも3バイトが必要です。しかし、これはやや多すぎます。24ビットの容量が提供されます。そのうち6ビットは未使用です。これらの6ビットはビット文字列の右端部に書き込まれるため、これは次のようにエンコードされます。

03 04 06 6e 5d c0

BERでは、未使用ビットは任意の値を持つことができます。そのため、そのエンコーディングの最後のバイトはc1、c2、c3などにすることができます。DERでは、未使用ビットはすべてゼロでなければなりません。

OCTET STRINGエンコーディング

OCTET STRINGは、含まれるバイトとしてエンコードされます。バイト03、02、06、A0を含むOCTET STRINGの例を示します。

04 04 03 02 06 A0

CHOICEおよびANYエンコーディング

エンコーディング命令で変更されない限り、CHOICEまたはANYフィールドは、実際に保持している型としてエンコードされます。そのため、ASN.1仕様のCHOICEフィールドでINTEGERまたはUTCTimeが許可され、エンコードされる特定のオブジェクトにINTEGERが含まれている場合、それはINTEGERとしてエンコードされます。

実際には、CHOICEフィールドは非常に多くの場合、エンコーディング命令を持っています。たとえば、RFC 5280のこの例では、rfc822NameとdNSNameの両方が基になる型IA5Stringを持っているため、エンコーディング命令はそれらを区別するために必要です。

   GeneralName ::= CHOICE {
        otherName                       [0]     OtherName,
        rfc822Name                      [1]     IA5String,
        dNSName                         [2]     IA5String,
        x400Address                     [3]     ORAddress,
        directoryName                   [4]     Name,
        ediPartyName                    [5]     EDIPartyName,
        uniformResourceIdentifier       [6]     IA5String,
        iPAddress                       [7]     OCTET STRING,
        registeredID                    [8]     OBJECT IDENTIFIER }

rfc822Name `a@example.com`を含むGeneralNameのエンコーディング例を示します([1]は、タグクラス「コンテキスト固有」(ビット8が1に設定されている)でタグ番号1を使用し、暗黙的タグエンコーディングメソッドを使用することを意味します)。

81 0d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d

dNSName "example.com"を含むGeneralNameのエンコーディング例を示します。

82 0b 65 78 61 6d 70 6c 65 2e 63 6f 6d

安全性

特にCやC++などのメモリセーフではない言語では、BERとDERのデコードには細心の注意を払うことが重要です。デコーダには多くの脆弱性の歴史があります。一般に入力を解析することは一般的な脆弱性の原因です。ASN.1エンコーディング形式は特に脆弱性の温床となっているようです。これらは複雑な形式であり、多くの可変長フィールドがあります。長さでさえ可変長です!また、ASN.1入力は多くの場合、攻撃者によって制御されます。承認されたユーザーと承認されていないユーザーを区別するために証明書を解析する必要がある場合、ある時は証明書ではなく、ASN.1コードのバグを悪用するために作成された奇妙な入力を解析していると想定する必要があります。

これらの問題を回避するには、可能な限りメモリセーフな言語を使用するのが最適です。そして、メモリセーフな言語を使用できるかどうかに関係なく、ASN.1コンパイラを使用して解析コードを生成するのが、ゼロから記述するよりも最適です。

謝辞

私はASN.1、DER、BERのサブセットに関する素人向けガイドに多大な恩義を負っています。これは私がこれらのトピックを学習した大きな要因です。DNSへの心温まる歓迎の著者にも感謝したいと思います。これは素晴らしい読み物であり、このドキュメントのトーンに影響を与えました。

ちょっとしたボーナス

PEMエンコードされた証明書が常に「MII」で始まることに気づいたことがありますか?たとえば

-----BEGIN CERTIFICATE-----

MIIFajCCBFKgAwIBAgISA6HJW9qjaoJoMn8iU8vTuiQ2MA0GCSqGSIb3DQEBCwUA
...

これで、その理由を説明できるようになりました!証明書はSEQUENCEであるため、「0x30」のバイトで始まります。次のバイトは長さフィールドです。証明書はほとんど常に127バイトより長いため、長さフィールドは長さのロングフォームを使用する必要があります。つまり、最初のバイトは0x80 + Nになり、Nは続く長さバイトの数です。Nはほとんど常に2です。これは、128から65535の長さをエンコードするのに必要なバイト数であり、ほとんどすべての証明書の長さがその範囲内にあるためです。

したがって、証明書のDERエンコーディングの先頭2バイトは0x30 0x82であることがわかります。PEMエンコーディングbase64を使用し、3バイトのバイナリ入力を4つのASCII出力文字にエンコードします。言い換えれば、base64は24ビットのバイナリ入力を4つのASCII出力文字に変換し、入力の6ビットが各文字に割り当てられます。すべての証明書の先頭16ビットがどうなるかを知っています。「(ほとんどの)すべての証明書の先頭文字が「MII」であることを証明するには、次の2ビットを見る必要があります。それらは、2つの長さバイトの最上位バイトの最上位ビットになります。これらのビットが1に設定されることはありますか?証明書が16,383バイトより長い場合を除いてはありません!したがって、PEM証明書の先頭文字は常に同じになると予測できます。自分で試してみてください。

xxd -r -p <<<308200 | base64