ハローワールド。
会社でプログラムを書いていたところ、とあるAPIの仕様書に
DigestInfo
と書かれていた項目がありました[1]。
証明書関連の文脈上に出てくるAPIであることと、昔この単語を聞いた覚えがあったのもあり、証明書関連の単語であり、おそらく署名対象のデータを表すものであることはすぐに分かりました。
最終的には下記のバイト列(正確にはそれをBase64エンコードしたもの)を送るのが正解だったのですが、DigestInfo
とだけ言われてから下記のバイト列まで行き着くまでの道程は非常に濃いものとなったため、今後似たような経験に遭遇した人たちのための標となることを祈って書き残し、私への手向けとします。
30 31 30 0d 06 09 60 86
48 01 65 03 04 02 01 05
00 04 20 2a f8 45 6a 33
37 19 04 86 e2 e0 12 26
87 fc 6f 99 63 08 df 58
98 8a 24 bb f0 36 7b 4d
1e 44 8d
ASN.1 との出会い
おもむろにGoogleの検索窓にDigestInfo
と入力し検索すると、下記のような記法が目につくと思います。
DigestInfo ::= SEQUENCE {
digestAlgorithm DigestAlgorithmIdentifier,
digest Digest }
これはRFC 2315 PKCS #7: Cryptographic Message Syntax Version 1.5[2]に記載されています。
同Section内では触れられていませんが、これはASN.1 (Abstract Syntax Notation One)と呼ばれる記法です。ASN.1は既存のデータ型を用いて新たなデータ型を定義する記法で、PKCSでも全体に渡ってデータを表現するのに利用されています。ASN.1自体も現在はX.680[3]シリーズで定義されています。
ASN.1には基本の型があり、例えば可視可能文字列(PrintableString)や整数型(INTEGER)、後ほど出てきますが何もないことを表すNULL型(NULL)などがあります。上記のDigestInfo
の定義で出てくるSEQUENCE
も基本形であり、これは順序を持った複数の値(型)を表すものです。つまり、DigestInfo
はDigestAlgorithmIdentifier
という型のdigestAlgorithm
とDigest
という型を持ったdigest
が連続した値、ということを表しています。
ではDigestAlgorithmIdentifier
とDigest
は一体何なんだと言うことです。どちらもX.680では規定されていないため、どこかに定義が記載されているはずです。
Digest
幸いDigest
に関してはRFC 2315内での上記の定義のすぐ下に記載があります。
Digest ::= OCTET STRING
OCTET STRING
はX.680にも規定があり、簡単に言えばバイト列を表すものです。なのでDigestは単純なバイト列であることがわかります。
当然、ただのバイト配列では駄目で、digest is the result of the message-digesting process.
と記載されています。つまりDigestはメッセージのハッシュ化した結果のバイト列であることがここでわかります。
ちなみに、オクテットとは8bitのことであり(octoは8って意味ですね)、現在は事実上1byteと同じ意味です。昔は1byteが4bitだったり6bitだったり環境依存だったらしいので、8bitの固定長を表すのにオクテットが利用されていました。本記事では今後バイト(byte)と記載します。
DigestAlgorithmIdentifier
問題はDigestAlgorithmIdentifier
です。名前から察するにハッシュ化アルゴリズムの特定するIDであることは間違いなさそうです。
その定義を確認しようと検索をかけてみると、RFC2315内ではSection 6.3に
DigestAlgorithmIdentifier ::= AlgorithmIdentifier
と記載されています。結局AlgorithmIdentifier
が何なのかという問題に移り変わっただけです。
Section 3ではAlgorithmIdentifierの定義が記載されています。
AlgorithmIdentifier: A type that identifies an algorithm (by object identifier) and associated parameters. This type is defined in X.509.
アルゴリズムのIDと関連パラメータを表すものらしいです。カッコの中に記載されているobject identifier
は後ほど現れてきます。
結局定義はX.509でされているようですね。
ここではX.509ではなく、X.509の証明書などについて記載されたRFC5280[4][5]を確認してみます。
Section 4.1.1.2にビンゴな定義が記載されています。
AlgorithmIdentifier ::= SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL }
AlgorithmIdentifier
はOBJECT IDENTIFIER
のalgorithm
とalgorithm
で定義されているOPTIONAL
(省略可能)なANY
(何でも入れていい型)であることがわかります。algorithm
はさておきとして、parameter
はその名の通りアルゴリズムに指定するパラメーターなのでしょう。
さてOBJECT IDENTIFIER
ですが、これはoidとも呼ばれるもので、ituが定めたオブジェクトを識別するためのIDのことを言っています。Wikipediaの記事の記載が詳しいです。
このOBJECT IDENTIFIER
、実はX.680の定義にも記載があるため、基本の型の1つです。
今回はハッシュ化アルゴリズムを指定します。OIDでは、ハッシュ化アルゴリズムは2.16.840.1.101.3.4.2
に定義されています。OID Repository[6]というサイトで検索してみると22個のハッシュ化アルゴリズムがあることがわかります。例えば、SHA256のOIDは2.16.840.1.101.3.4.2.1
であることがわかります。
このドットで区切られた数字はそれぞれに意味があります。ASN.1で記載する場合は下記になりますが、それぞれの数字の意味がわかりやすいため、説明の代わりとします。
{joint-iso-itu-t(2) country(16) us(840) organization(1) gov(101) csor(3) nistAlgorithms(4) hashalgs(2) sha256(1)}
ASN.1上でのDigestInfo
というわけで、ここまでくればDigestInfoが何を表しているのかはわかります。ASN.1記法で書くならこうなります。
DigestInfo ::= SEQUENCE {
digestAlgorithm SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL },
digest OCTET STRING }
この時algorithm
はハッシュアルゴリズムを指定します。例えばSHA256であれば{joint-iso-itu-t(2) country(16) us(840) organization(1) gov(101) csor(3) nistAlgorithms(4) hashalgs(2) sha256(1)}
ということですね。そして、digest
は、そのハッシュアルゴリズムでハッシュ化した値(署名対象データ)であることが必須です。
ここまで来るとDigestInfo
がどういったデータなのかはわかったかと思います。
ASN.1からDER(BER)への変換
ここまで来て何を送れば良いのかはわかりましたが、どうやって送れば良いのか、それに関しては仕様書には一切記載がありません。この仕様書だけでどうやって俺はデータを送れば良いんだよ。
幸いデータ例があったので、これはDER(正確にはBERかもしれない)で変換して送信することがわかりました。
DER
証明書関連でPEMという言葉がよく出てきます。これは証明書のエンコーディングの1つです。このPEMは大体下記のような形式です。
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvKTZNwTucziHtmipgWRL
...
L9ZH1+DduRy2bextwOnBw0K+nSptpT8PDy6U24UO2PgIgh13evVmaznwp0hFig4B
dwIDAQAB
-----END PUBLIC KEY-----
この中身はBase64でエンコードされていますが、この元データはDERでエンコードされた証明書データです。そして、この証明書データはASN.1の形となっています。
DERはASN.1の形式で表記されたデータ構造をバイト列にエンコードするルールです。X.690ではASN.1のエンコードルールとして、BER, CER, DERについての定義がなされています。流石にプロダクションコードに自前のエンコーディングを入れるつもりはないので、詳しくは調べていないですが軽く説明を入れていきます。
ASN.1形式のデータをDERで変換する際、基本的にTAG + LENGTH + VALUEの3つのバイト列を連結して1つのデータを表します。
TAGというのは、ASN.1の型です。例えばOCTET STRING
であればタグは04
(16進数)です。また、SEQUENCE
は、タグ自体は10
(16進数)ですが、構造を持つ型なので6bit目に1が立ちます。結果として、タグは30
(16進数)となります。ANS.1 のタグ一覧に全体がまとまっているため詳細はお任せいたします。
LENGTHはその名の通りVALUEの長さを表します。VALUEの長さが128byte未満であれば、LENGTHにはその数字が入ります。128byte以上であれば、まず1byte目にLENGTHを表すのに利用するbyteを記載し、2byte目以降にVALUEのLENGTHを記載します。その際最初のbyteのbit8を1とします。
VALUEは単純に値です。OCTET STRINGであれば単純にそのままバイト列となります。SEQUENCE等構造化データの場合は中身が更に別の値、つまりTAG + LENGTH + VALUEの形のデータとなります。
DERで変換したoid
DigestInfoにはoidであるalgorithm
があります。oidをDERで表現する時、ちょっと複雑な動きをします。
具体的にはC#でASN.1のObject Identifierのエンコードを行うに詳しいのですが、VALUEが素直にOIDをバイト列に直したものではないのです。
OIDは最初は必ず、0, 1, 2のどれかなので、最初の値に40(=0x28)をかけ(つまり1なら40, 2なら80)、そこにOIDの2つ目の値を足し合わせます。
例えばSHA256のOIDの最初2つは2.16
です。なのでとなります。
また、SHA256ではOIDの中に840と128より大きな値があります。128より大きな値を記載する場合は、まず2進数で7bitごとに分割し、最後のbyteを除いてbit8の1を立てます。
例えば840では7bitづつに分割して(0b00000110 0b01001000 = 0x06 0x48)、最後のbyte以外のbit8を立てる(0b10000110 0b01001000 = 0x86 0x48)ということなので、最終的には86 48
となります。
それ以外は普通にバイトの値として組み合わせればいいので、SHA256を表すoid2.16.840.1.101.3.4.2.1
はDERでエンコードすると60 86 48 01 65 03 04 02 01
の9byteとなります。
oidはDERのTAGでは06
に割り振られているので、合わせせると06 09 60 86 48 01 65 03 04 02 01
というのがOIDを表すものです。
DERで変換したDigestInfo
ここまで来たら怖いものはありません。知っている知識フル活用でもうDigestInfoをDERで変換してやれば良いのです。
今一度ASN.1でのDigestInfoを確認してみましょう。
DigestInfo ::= SEQUENCE {
digestAlgorithm SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL },
digest OCTET STRING }
先程algorithm
は作りました。parameters
はOPTIONAL
で不要でしょう(そしてこれが後ほど間違いとして時間をつぶすことになります)。
SEQUENCE
のTAGは先程記載したとおり30
です。そして中身はalgorithm
だけなのでVALUEの9byte + TAGとLENGTHの2byteで長さは11 = 0x0bです。なのでdigestAlgorithm
は
30 0b (SEUQUENCE)
06 09 (OID)
60 86 48 01 65 03 04 02 01
となります。
digest
に関しては何でも良いので、今回はdigest test
という文字列をsha256でハッシュ化してみました。結果は2af8456a3337190486e2e0122687fc6f996308df58988a24bbf0367b4d1e448d
です。
なのでdigest
をDERで変換すると、OCTET STRINGのTAGは04
で、LENGTHはSHA256なので当然256bit = 32byte = 0x20 byteなので20
です。
よってdigest
をDERで変換すると下記となります。
04 20(OCTET STRING)
2a f8 45 6a 33 37 19 04
86 e2 e0 12 26 87 fc 6f
99 63 08 df 58 98 8a 24
bb f0 36 7b 4d 1e 44 8d
最後に全部組みわせます。SEQUENCEのTAGは30
そして、LENGTHは41 = 0x29なので、
DigestInfo ::= SEQUENCE {
digestAlgorithm SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL },
digest OCTET STRING }
は
30 29 (SEQUENCE)
30 0b (SEUQUENCE)
06 09 (OID)
60 86 48 01 65 03 04 02 01
04 20 (OCTET STRING)
2a f8 45 6a 33 37 19 04
86 e2 e0 12 26 87 fc 6f
99 63 08 df 58 98 8a 24
bb f0 36 7b 4d 1e 44 8d
となります(実は違いました)。
parametersの仕様
上記の値でテストを行ってみたところ何故か動かないのです。実は digestAlgorithm
の parameters
の指定に罠(自分で勝手に引っかかった)がありました。
実はPKCS #1のRFCであるRFC8017[7]のAppendix A.2.4にはDigestInfoの定義が書いてあります。上記に記載されているものとは異なります。
DigestInfo ::= SEQUENCE {
digestAlgorithm DigestAlgorithm,
digest OCTET STRING
}
DigestAlgorithm ::= AlgorithmIdentifier {
{PKCS1-v1-5DigestAlgorithms}
}
PKCS1-v1-5DigestAlgorithms ALGORITHM-IDENTIFIER ::= {
{ OID id-md2 PARAMETERS NULL }|
{ OID id-md5 PARAMETERS NULL }|
{ OID id-sha1PARAMETERS NULL }|
{ OID id-sha224 PARAMETERS NULL }|
{ OID id-sha256 PARAMETERS NULL }|
{ OID id-sha384 PARAMETERS NULL }|
{ OID id-sha512 PARAMETERS NULL }|
{ OID id-sha512-224 PARAMETERS NULL }|
{ OID id-sha512-256 PARAMETERS NULL }
}
ここまで来たら、これぐらいのASN.1表記はスラスラ読めると思いますが、一番重要のはOID id-sha256 PARAMETERS NULL
です。つまりこれは「パラメータにnullをいれろ」という記載です。
ただしその次には
When id-sha1, id-sha224, id-sha256, id-sha384, id-sha512, id-sha512-224, and id-sha512-256 are used in an AlgorithmIdentifier, the parameters (which are optional) SHOULD be omitted, but if present, they SHALL have a value of type NULL. However, implementations MUST accept AlgorithmIdentifier values both without parameters and with NULL parameters.
AlgorithmIdentifier に id-sha1、id-sha224、id-sha256、id-sha384、id-sha512、id-sha512-224、id-sha512-256 を使用する場合、(オプションである) パラメータは省略すべきである(SHOULD)が、存在する場合は NULL タイプの値を持たなければならない(SHALL)。ただし、実装ではAlgorithmIdentifierの値を、パラメータなしでもNULLパラメータ付きでも受け入れなければなりません(MUST)。
と記載もあります(強調は筆者)。
RFC的には入れても入れなくても問題はないようですが、結局動かないものは動かないのでparameterにNULLを入れて送らなければならないようです。
nullと空では、実はDER形式では少し異なります。 DER形式ではNULL型という型があります。TAGは05
、通常はLENGTHを00
(つまりVALUEが空のデータ)として表現するようです。
最終的なDERで変換したDigestInfo
よってparameter
に05 00
が入り、それでLENGTHが多少変わるので
DigestInfo ::= SEQUENCE {
digestAlgorithm SEQUENCE {
algorithm OBJECT IDENTIFIER,
parameters ANY DEFINED BY algorithm OPTIONAL },
digest OCTET STRING }
は
30 31 (SEQUENCE)
30 0d (SEUQUENCE)
06 09 (OID)
60 86 48 01 65 03 04 02 01
05 00 (NULL)
04 20 (OCTET STRING)
2a f8 45 6a 33 37 19 04
86 e2 e0 12 26 87 fc 6f
99 63 08 df 58 98 8a 24
bb f0 36 7b 4d 1e 44 8d
となりました。これは最初に提示したバイト列と同じです。
さいごに
DigestInfoという単語(とたった1つのデータ例)から正しいデータ列をいつでも生成する力を手に入れることができました。
こういった証明書やPKIなどでは、RFCやX.509などの定義を参照していればほぼ問題がないと思っていました。しかし今回の例のように歴史的な経緯があったり、またバージョンによって大幅な転換があったりと、一筋縄では決して行かない難しさを感じました。
とはいえ、ASN.1やDERといった知識はこういった部分では必須知識と考えて問題はないと思います。詳しく知ることができたこの(余り親切ではない)設計書に感謝ですね。
参考文献
- RFC2315
- RFC5280
- オブジェクト識別子 - Wikipedia
- 抽象記法
- ANS.1 のタグ一覧 | 晴耕雨読
- C#でASN.1のObject Identifierのエンコードを行う - Qiita
- rfc8017
- rfc5754
正確に言うと、DigestInfoだけではないですが、仕様書の記載者がそのあたり理解していなかったためか、全く間違っていた記載がされていたため少しお茶を濁しています。 ↩︎
https://www.itu.int/itu-t/recommendations/rec.aspx?rec=x.680 ↩︎
RFC5280は有名なRFCの1つでもあるので、日本語訳も存在します。IPAからも出ているため(https://www.ipa.go.jp/security/rfc/RFC5280-00JA.html)こちらを読むのもいいでしょう。 ↩︎