RSA 秘密鍵/公開鍵ファイルのフォーマット

openssl コマンドで生成される RSA 秘密鍵ファイルのフォーマットの中身が気になったので調べてみた。
初心者にわかりやすく説明されたサイトが意外と見当たらなかったようなのでまとめておく。

まず、鍵の生成に使ったコマンドはこんな感じ:

$ openssl genrsa 2048 > rsaprivate.key

2048 というのは鍵の bit 数。

以下の説明では、あまり長い鍵だとわかりにくくなっちゃうので短くして 256 bit の鍵にしてみた。
よい子のみんなはこんな短い鍵は使っちゃダメだよ。


256 bit の秘密鍵ファイルをダンプしてみるとこんな感じ:

$ cat rsaprivate.key
-----BEGIN RSA PRIVATE KEY-----
MIGrAgEAAiEAvnrd8LBnzAGxCW+i7KtVQSiTsssMtbwcs5styeKsn2kCAwEAAQIh
AKBF8glb5Xqa0cQG0ygg4hIFdipmvEJhiCuhX93krDCBAhEA51bAM0gFPvxyk9Xe
ioIOBQIRANLJEv4Xw7MwT7EEEARL5RUCEBa8bu1bUbCsDPK8nT+NoqUCEQCIzFCU
MY4j7BW8N3vBnhPlAhBgs4tSfe6RbpertixmCygk
-----END RSA PRIVATE KEY-----

秘密鍵の中身は本当はこんな公の場に晒しちゃいけないけど、この鍵は単に説明用に生成しただけで、実際には何にも使わないから心配しないでね。

現時点で主流の 2048 bit だともっと長くなるよ。

このファイルのフォーマットは PEM 形式といって、固定のヘッダ・フッタの間に BASE64エンコードされたデータが入っている。

こんなイメージ:

-----BEGIN RSA PRIVATE KEY-----
BASE64 ENCODED DATA
-----END RSA PRIVATE KEY-----


BASE64 をデコードして、元のデータを取り出してみよう。

$ cat rsaprivate.key | awk /^[^-]/{print} | base64 -d

awk でヘッダとフッタを取り除いたあとにデコードをしているよ。

なにやら文字化けしたようなデータが表示されてしまったかな?
この、デコードして得られた元のデータは、秘密鍵の情報を DER (Distinguished Encoding Rule) という形式でエンコードしたバイナリになっているらしい。

その DER のデータを読み解くと、秘密鍵の情報が得られるんだけど、その内容は ASN.1 という規則に則って以下のような Syntax だと定められているよ。

RSAPrivateKey ::= SEQUENCE {
    version           Version,
    modulus           INTEGER, -- n
    publicExponent    INTEGER, -- e
    privateExponent   INTEGER, -- d
    prime1            INTEGER, -- p
    prime2            INTEGER, -- q
    exponent1         INTEGER, -- d mod (p-1)
    exponent2         INTEGER, -- d mod (q-1)
    coefficient       INTEGER, -- (inverse of q) mod p
    otherPrimeInfos   OtherPrimeInfos OPTIONAL
}

参考(1): PKCS#1
参考(2): ITU-T X.690 PKCS#1

ここで、version は INTEGER 型なんだけど、取りうる値の範囲が 0 か 1 しかなくて、1 だと otherPrimeInfos があるけど 0 だと存在しないよ。


それじゃ、さっきデコードしたデータをとりあえず16進ダンプしてみよう。

$ xxd <(cat rsaprivate.key | awk /^[^-]/{print} | base64 -d)
0000000: 3081 ab02 0100 0221 00be 7add f0b0 67cc  0......!..z...g.
0000010: 01b1 096f a2ec ab55 4128 93b2 cb0c b5bc  ...o...UA(......
0000020: 1cb3 9b2d c9e2 ac9f 6902 0301 0001 0221  ...-....i......!
0000030: 00a0 45f2 095b e57a 9ad1 c406 d328 20e2  ..E..[.z.....( .
0000040: 1205 762a 66bc 4261 882b a15f dde4 ac30  ..v*f.Ba.+._...0
0000050: 8102 1100 e756 c033 4805 3efc 7293 d5de  .....V.3H.>.r...
0000060: 8a82 0e05 0211 00d2 c912 fe17 c3b3 304f  ..............0O
0000070: b104 1004 4be5 1502 1016 bc6e ed5b 51b0  ....K......n.[Q.
0000080: ac0c f2bc 9d3f 8da2 a502 1100 88cc 5094  .....?........P.
0000090: 318e 23ec 15bc 377b c19e 13e5 0210 60b3  1.#...7{......`.
00000a0: 8b52 7dee 916e 97ab b62c 660b 2824       .R}..n...,f.($


さっきも言ったけどこのバイナリデータは DER というフォーマットになっている。
DER フォーマットは、ASN.1 というツリー構造のデータをバイナリに変換する(シリアライズする)ときに使われるフォーマットの一つで、同じデータから複数の表現が出てこないようになっているらしい。

例えば整数の表現は DER では可変長なんだけど、1 という数(データ)を表現するときには可変長なので 0x01 とも表現できるし 0x00000001 とも表現できるはずなんだけども、DER ではオクテット(バイト)単位で一番短くなる 0x01 しか認めていないんだ。

秘密鍵のバイナリデータが違うから違う鍵だと思っていたら実は同じ鍵だった!なんてことがあったら悪い人に悪用されて困ったことになりそうだから、きっとそうならないようにしているのかな。


DER フォーマットは基本的
ID(可変長)、コンテンツの長さ(可変長)、コンテンツ
という、いわゆる TLV の繰り返し・入れ子の構造になっているよ。

ID は本当は可変長なんだけど、RSA 秘密鍵のファイルの場合は 1 バイト固定だと思って構わないよ。

ID の 1 バイトめは、さらにビットごとに以下のようにわかれている。

bit 7 6 5 4 3 2 1 0
クラス プリミティブ型か複合型か タグ

ここでタグの値が 31(5 bit 全部 1)なら後続のオクテットがあって可変長になるんだけど、RSA 秘密鍵ではここが 31 になることはないよ。

クラスというのは、以下の表のようになっていて、タグが公式のものかはたまたプライベートなものか、というようなことを表しているよ。
RSA 秘密鍵では Universal しか出てこなくて、タグは全部公式な規格で定められたものしか使われていないよ。

bit 7 bit 6 意味
0 0 Universal
0 1 Application 固有
1 0 Context 固有
1 1 Private

bit 5 は、コンテンツの型がプリミティブ型なのか複合型なのかを表していて、0 だとプリミティブ型、1 だと複合型になる。
複合型というのは、プリミティブや他の複合型を組み合わせて作られる型で、C 言語でいう構造体とか配列みたいなものだと思っておけば OK。

というわけで、最初に ID の 1 バイトを読むと、そのコンテンツがどんな型のデータなのかがわかる。

さっきの例だと先頭の1バイトは 0x30 になっているね。

bit 7 bit 6 bit 5 bit 4 3 2 1 0
0 0 1 1 0 0 0 0

こうだね。

bit 5 が 1 になっているのでこれは複合型。
タグが 0x10 なので、コンテンツは SEQUENCE 型という複合型になっていることがわかる。
(タグの値と型の対応は ITU-T Rec. X.680 のセクション 8.4 にある Table 1 - Universal class tag assignments に定められているよ。)


SEQUENCE は、コンテンツの中に TLV がまた現れて、その出現順も決められているときに使われる複合型だよ。
秘密鍵の場合は n とか e とか p とか q とかの記録されている順番が大事だから SEQUENCE になっているんだね。


さて、つぎはその SEQUENCE の長さを見てみよう。
「長さフィールド」の長さ自体も可変長なんだけど、これは最初の1バイトを読んでみれば全体で何バイト読み取ればよいのかがわかるよ。

さっきの例だと 0x30 の次は 0x81 になっているね。

0x81

bit 7 6 5 4 3 2 1 0
1 0 0 0 0 0 0 1

0x81 は最上位ビット(bit 7)が立っているので、長さフィールドは1バイトに収まっていないことを意味する。逆に最上位ビットが立っていない場合はその1バイト自体がコンテンツの長さになるよ。
最上位ビット以外のビット(bit 6〜0)を見ると、0x01 になっている。

これは続く 1 バイトで長さを表しているということになる。

なので次の1バイトを読む。
0xab だね。

つまり、この SEQUENCE 型のコンテンツは 0xab(= 171 バイト)だっていうこと。
ちなみに 0xab の次のバイトから 171 バイト読み進めると、EOF に到達するね。
つまりこのデータにはこの SEQUENCE 型しか入っていないことになる。

コンテンツの解読を進めていこう。

SEQUENCE 型のコンテンツは、TLV が再び繰り返し入っていると説明したね。
でも各 TLV は可変長なので、何個の TLV が入っているかはよくわからない。
RSA 秘密鍵の ASN.1 の定義からは、少なくとも 9 個の INTEGER 型のデータが入っているはずだけどね。

じゃあ早速最初のデータの TLV を見てみよう。

0xab の次のバイトは 0x02 になっているね。
この ID は、Universal クラス、Primitive 型で INTEGER 型のタグを持っている。

つまり、SQEUQNCE の中に入っている最初の TLV は INTEGER 型のデータだってこと。

長さを見てみよう。
まずは1バイト読み取る。0x01 だね。最上位ビットが立っていないので、この 0x01 という値そのものがコンテンツの長さを表している。
つまりこの INTEGER 型は 1 バイトのデータだということがわかる。

じゃあ次の 1 バイトのコンテンツを読み取ろう。0x00 だね。この INTEGER 型の値は 0 ということだ。

RSA 秘密鍵の ASN.1 による定義を見てみると、これは version = 0 ということに相当するね。

次の TLV を見てみよう。
つぎの ID も 0x02 だ。これまた INTEGER 型。
長さは 0x21 つまり 33 バイト。

00 be 7a dd f0 b0 67 cc 01 b1 09 6f a2 ec ab 55
41 28 93 b2 cb 0c b5 bc 1c b3 9b 2d c9 e2 ac 9f
69

33バイトの整数って、ピンと来ないかもしれないけど、いわゆる多倍長整数というやつだね。
256 bit の鍵なので、32バイトで十分なんだけど、最上位ビットが立ってしまうのを防ぐために最上位に 0 のバイトをくっつけているので 33 バイトになってしまうんだね。

これは RSA 秘密鍵の modulus が 0x00be7addf0b067cc01b1096fa2ecab55412893b2cb0cb5bc1cb39b2dc9e2ac9f69 という多倍長整数だということだね。大きすぎてよくわからないけど、10進数に直すと
86156528347631080411887474184796200056952828735035413778633685555551033663337
という数みたいだよ。

256 bit の鍵でこんな大きな数になるのに、この程度の長さの鍵だとコンピュータの力をもってすれば解くことができてしまうんだね。


さて、33バイト読み取った次のデータは何だろう。
また ID は 0x02 だけど今度は長さは 0x03 だね。3バイトの整数ということだ。
3バイトは 0x01 0x00 0x01 になっているよ。

これは RSA 秘密鍵の e (publicExponent) に相当するね。
0x010001 = 65537 だよ。

同様にして、最後まで読んでいくと
d (privateExponent) = 0x00a045f2095be57a9ad1c406d32820e21205762a66bc4261882ba15fdde4ac3081
p (prime1) = 0x00e756c03348053efc7293d5de8a820e05
q (prime2) = 0x00d2c912fe17c3b3304fb10410044be515
d mod (p-1) (exponent1) = 0x16bc6eed5b51b0ac0cf2bc9d3f8da2a5
d mod (q-1) (exponent2) = 0x0088cc5094318e23ec15bc377bc19e13e5
(inverse of q) mod p (coefficient) = 0x60b38b527dee916e97abb62c660b2824
ということがわかるね。

簡単だったでしょ?



ちなみに、こんな面倒なことをしなくても、

$ openssl asn1parse -inform PEM < rsaprivate.key 
    0:d=0  hl=3 l= 171 cons: SEQUENCE          
    3:d=1  hl=2 l=   1 prim: INTEGER           :00
    6:d=1  hl=2 l=  33 prim: INTEGER           :BE7ADDF0B067CC01B1096FA2ECAB55412893B2CB0CB5BC1CB39B2DC9E2AC9F69
   41:d=1  hl=2 l=   3 prim: INTEGER           :010001
   46:d=1  hl=2 l=  33 prim: INTEGER           :A045F2095BE57A9AD1C406D32820E21205762A66BC4261882BA15FDDE4AC3081
   81:d=1  hl=2 l=  17 prim: INTEGER           :E756C03348053EFC7293D5DE8A820E05
  100:d=1  hl=2 l=  17 prim: INTEGER           :D2C912FE17C3B3304FB10410044BE515
  119:d=1  hl=2 l=  16 prim: INTEGER           :16BC6EED5B51B0AC0CF2BC9D3F8DA2A5
  137:d=1  hl=2 l=  17 prim: INTEGER           :88CC5094318E23EC15BC377BC19E13E5
  156:d=1  hl=2 l=  16 prim: INTEGER           :60B38B527DEE916E97ABB62C660B2824

これで一発だよ。


ちなみに、公開鍵っていうのは秘密鍵から modulus と publicExponent だけを抜き出したものだよ。

だから、ここまで来ることができたあなたは、公開鍵の PEM ファイルも読めるようになったはずだよ。