電子署名のファイルフォーマット
OpenSSLのコマンドを使って電子署名を生成する方法は、OpenSSLコマンドによる公開鍵暗号、電子署名の方法 で解説しました。
そのコマンドで生成された sign.sig
というファイルの中身は一体なんなのでしょうか?というのをまとめたのがこのエントリです。
電子署名に関する仕様
RSA暗号の全体像で解説したように、公開鍵暗号に関する仕様は PKCSという規格群にまとめられています。
電子署名のフォーマットも PKCS #1に規定があります。PKCS #1は RFC 3447として公開されているので、無料で読むことができます。
また、PKCS #7にも電子署名に関する記述がありそうです。こちらはまだちゃんと読めていませんが、どうやら、署名自体を元のドキュメントに添付する方法などについて規定しているようです。
ですので、電子署名のフォーマットについては、まずは PKCS #1を見るのが先決です。
PKCS #1 から電子署名の仕様を読み解く
「8 Signature schemes with appendix」
Section 8に、署名のスキームについての説明があります。 with appendix は直訳すると「付加情報付き」という意味ですが、おそらく、署名を検証するための付加情報がついている、ということを言いたいにだろうと思います。
この説明によると、電子署名には以下の2つのスキームがあるようです。
- RSASSA-PSS
- RSASSA-PKCS1-v1_5
これらについては以下のように説明されています。
Although no attacks are known against RSASSA-PKCS1-v1_5, in the interest of increased robustness, RSASSA-PSS is recommended for eventual adoption in new applications.
(適当訳)
RSASSA-PKCS1-v1_5に対する有効な攻撃は知られていませんが、新しいアプリケーションでは堅牢性を向上させる目的で RSASSA-PSSを採択することを推奨します。
ということで、RSASSA-PSSがとても良いもののように読めますが、残念ながら opensslが対応しているのは RSASSA-PKCS1-v1_5 の方のようです。PSSの方の仕様を読んでみると、割とめんどっちいことがいろいろ書かれているので、特に問題ないなら v1_5のままでいいよねって感じなのかもしれません。
実際、OpenSSL以外の暗号化ライブラリなども v1_5をサポートしているものがほとんどのようです。なのでここからは RSASSA-PSSのことは忘れて、v1_5の方だけ見ていきます。
「8.2.1 Signature generation operation」
8.2.1に、RSASSA-PKCS1-v1_5 の署名生成処理の詳細が書かれています。適当に訳してみます。
RSASSA-PKCS1-V1_5-SIGN (K, M)
-
入力:
- K - 署名者の秘密鍵
- M - 署名対象のメッセージ(バイナリデータ)
-
出力:
- S - 署名(長さkのバイナリデータ)。kは RSAモジュールの n以下の長さ(?)
-
エラー:
- "message too long"
- "RSA modulus too short"
####手順:
-
EMSA-PKCS1-v1_5 エンコーディングを行う
エンコードされたものをEMとする。エンコードの詳細は9.2で説明。
EM = EMSA-PKCS1-V1_5-ENCODE (M, k).
-
RSA署名を行う
a. 4.2で定義されたOS2IPを使って、エンコードされたメッセージEMを整数値 mに変換する。(訳注:OS2IPは、単にバイナリ列を符号なしの巨大なビッグエンディアンの整数値とみなすというだけです)
m = OS2IP (EM).
b. RSA秘密鍵 Kとメッセージ m を RSAAP1に適用し、整数値の署名値 sを生成する(5.2.1節参照)(訳注:単に、秘密鍵Kでmを暗号化する処理と思われます)
s = RSASP1 (K, m).
c. 4.1で定義されたI2OSPをつかって、署名値 sを長さ kのバイナリデータに変換する。(訳注:I2OSPは、単に正の整数値を符号なしkバイトのビッグエンディアンで書き出す処理です)
S = I2OSP (s, k).
-
署名 Sを出力する
ということで、2は単に「秘密鍵で暗号化する」処理を厳密に書いているだけなので、問題は1の EMSA-PKCS-V1_5-ENCODE の処理がなんなのかというところに絞られます。これは 9.2節を見てみればわかります。
「8.2.2 Signature verification operation」
8.2.2では、8.2.1で規定された RSASSA-PKCS1-V1_5-SIGN の逆処理、RSASSA-PKCS1-V1_5-VERIFY の動作が規定されています。
「9.2 EMSA-PKCS1-v1_5」
8.2.1のRSASSA-PKCS1-V1_5-SIGN で使われている EMSA-PKCS1-v1_5-ENCODE の仕様が書かれているので、こちらも訳してみます。
EMSA-PKCS1-v1_5-ENCODE (M, emLen)
-
オプション
- Hash - ハッシュ関数 (hLen はハッシュ関数が出力するハッシュ値のバイト長を表す)
-
入力:
- M - エンコードするメッセージ
- emLen - エンコードされたメッセージ(EM)の長さ。最低でも tLen + 11 である必要がある。ここで、tLenは、エンコード処理の過程で生成されるTを DERエンコードしたバイト長である。(訳注: このエンコードメソッドではパディングによって長さを調整できるので、emLenに好きな長さを指定できる。ただし、短すぎると入りきらないのでダメですよ、ということ)
-
出力:
- EM - エンコードされたメッセージ(長さ emLenのバイナリデータ)
-
エラー:
- "message too long"
- "intended encoded message length too short"
手順:
-
ハッシュ関数をメッセージMに適用して、ハッシュ値 H を生成する:
H = Hash(M).
もし、ハッシュ関数が "message too long" を出力したら "message too long" を出力して停止する
-
ハッシュ関数に対応したアルゴリズムIDとハッシュ値を ASN.1の DigestInfo型の値として表現し、DERエンコードする。なお、DigestInfo型は次の構造をしている。
DigestInfo ::= SEQUENCE { digestAlgorithm AlgorithmIdentifier, digest OCTET STRING }
最初のフィールドはハッシュ関数のIDで、二番目はハッシュ値を示している。
ここで、
- T - DERエンコードされた DigestInfo値
- tLen - Tのバイト長
と定義する。
-
もし、emLen < tLen + 11 であるならば、"intended encoded message length too short" を出力して停止する。
-
長さ emLen - tLen - 3 バイトの0xff値からなるバイナリデータ PS を生成する。ここで PSは最低でも 8バイトの長さがあるはずである。
-
PS、DERエンコードされた T、その他のパディングを結合して、以下のEMを生成する。
EM = 0x00 || 0x01 || PS || 0x00 || T.
-
EMを出力する
メモ:
-
Appendix B.1で言及している6つのハッシュ関数について、DigestInfoを DERエンコードした値はそれぞれ以下のようになる:
-
MD2:
(0x)30 20 30 0c 06 08 2a 86 48 86 f7 0d 02 02 05 00 04 10 || H.
-
MD5:
(0x)30 20 30 0c 06 08 2a 86 48 86 f7 0d 02 05 05 00 04 10 || H.
-
SHA-1:
(0x)30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14 || H.
-
SHA-256:
(0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 || H.
-
SHA-384:
(0x)30 41 30 0d 06 09 60 86 48 01 65 03 04 02 02 05 00 04 30 || H.
-
SHA-512:
(0x)30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00 04 40 || H.
-
-
このドキュメントのバージョン1.5では、TはDigestInfo値をDERエンコードしたものではなく、BERエンコードしたものと定義されていた。そのため、このドキュメント(バージョン2.0)で定義される署名の検証処理において、PKCS #1 v1.5 仕様においては正しいとされる署名を拒絶する可能性がある。例えば、長さが不定になるシーケンス型のエンコードのようなDER以外のルールが DigestInfoに適用された場合に起こります。実際にはこのようなことが心配になることはありませんが、注意深い実装者は、PKCS #1 v1.5で定義された BERエンコーディングをベースにした検証処理を採用しても構わない。このようにして、PKCS #1 v1.5との互換性が得られる。そのような検証処理は、使われているBERエンコーディングがDERエンコーディングであり、それゆれ、署名がこのドキュメントで定義された仕様に沿っていることを明示すべきである。
結局、電子署名のフォーマットはどうなってるの?
仕様書なので仕方ないですが、ちょっとわかりにくいですよね。
なのでわかりやすく図解しよう!と意気込んでいたら、すでに作っている方がいました! こちらの 「図説RSA署名の巻」というページです。仕様書の各ステップで、具体的にどのような処理が行われるのかを、わかりやすく図にしてくれています。
OpenSSLコマンドとの対応
では、上記仕様と、OpenSSLコマンドによる公開鍵暗号、電子署名の方法 で行った OpenSSLコマンドによる署名処理とはどういう関係があるのでしょうか?
openssl dgst -sha1 -sign との関係
openssl dgst -sha1 -sign
コマンドは、上記仕様の RSASSA-PKCS1-V1_5-SIGN (K, M)
に対応します。
> openssl dgst -sha1 -sign private-key.pem target.txt > sign.sig
というコマンドを入力すると、private-key.pemが K、target.txtが M として RSASSA-PKCS1-V1_5-SIGN (K, M)
が実行され、EMが出力されます。
openssl rsautl -sign との関係
実は、OpenSSLのコマンドにはもう一つ署名に関するコマンドがあります。それが、 openssl rsautl -sign
です。例えば以下のようにすると電子署名が出力されます。
> openssl rsautl -sign -inkey private-key.pem -in target.txt > sign2.sig
sign.sigと sign2.sigの中身を見てみると、全く内容が異なります。
> hexdump sign.sig
0000000 ac d0 75 ab 62 06 9a 3a 9b c4 c4 91 40 be 9d 64
0000010 58 4d 64 af 75 55 35 4f f3 20 c5 de 34 d2 30 af
0000020 5f db 91 cc 3d 2a ea 86 32 39 7a b0 a3 09 95 ed
0000030 60 dc 91 1b 1e 14 dd b2 78 65 ed 97 17 fa 05 e5
0000040 7e eb 4f 64 99 31 1c 8e d8 91 d6 b5 76 c7 df f8
0000050 6e 1a 32 24 a6 2a 81 5c 52 54 f1 1c de 0a a5 00
0000060 47 37 ee 8e 8b 57 34 ea 21 f5 d5 b0 e0 6c 84 2b
0000070 8b a2 2a e9 94 f1 f6 a7 38 0d 9d e7 3e 61 81 93
0000080
> hexdump sign2.sig
0000000 6a 5f 9a 7e e5 a0 2b d8 67 3e f7 cf 89 5e 2e 14
0000010 37 58 74 31 a2 8f 52 b7 28 25 1e ab b5 5c a8 92
0000020 78 2a c3 66 56 97 9f fa ff da 7b 4a 2c 9a 6b ba
0000030 34 de 0b ed 52 87 63 0e 11 49 b0 83 cc f2 72 c7
0000040 e1 4d 96 5c c9 f3 3f d5 cf ec 36 83 15 b6 22 9a
0000050 a0 d1 09 5f 7a 5b 9a 4c b3 ea cf fe f4 7d 4c b4
0000060 b1 21 36 99 b9 7e f7 bb 7a d4 77 90 84 0b c4 74
0000070 f2 d0 60 91 b5 bf 95 01 40 9b 70 4f 5a 0f 09 71
0000080
同じ鍵で、同じファイルに対して行ったのに、なぜ全く違うものが出力されるのでしょう?実はこの違いが電子署名を扱う上での 非常にやっかいな罠 になっています。
上記2つがどう違うのか調べてみる必要があるのですが、どちらもおそらく秘密鍵で暗号化された情報なので、これを眺めていても何も見えてきません。困りました。
ところで、sign2.signを生成するのに使った openssl rsautl -sign
は署名を生成するコマンドですが、それと対になるコマンドは ```openssl rsautl -verify`` です。試しに、これを両者に対してかけてみます。
> openssl rsautl -verify -pubin -inkey public-key.pem -in sign.sig | hexdump -C
00000000 30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14 ee |0!0...+.........|
00000010 fd 5b c2 c5 47 bf 82 b1 77 b6 25 9c 13 f7 72 3d |.[..G...w.%...r=|
00000020 c8 76 d9 |.v.|
00000023
> openssl rsautl -verify -pubin -inkey public-key.pem -in sign2.sig | hexdump -C
00000000 68 6f 67 65 0a |hoge.|
00000005
なんと、何かがデコードされてバイナリとして出力されてきました。
sign.sigのデコード結果を読み解く
sign.sigを verifyした結果をよく見てみると、0x30 21 30 09 となっていて、先ほど 9.2 EMSA-PKCS1-v1_5 のところで出てきた、SHA-1の DigestInfo構造になっているように見えます。
つまり、
00000000 30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14
までがSHA-1のAlgorithmIdentifier(のDERエンコード)で、
00000000 ee
00000010 fd 5b c2 c5 47 bf 82 b1 77 b6 25 9c 13 f7 72 3d
00000020 c8 76 d9
の20バイトが H、つまり実際のSHA-1ハッシュ値になっているようです。実際、target.txtのSHA-1ハッシュ値を計算してみると、
> shasum target.txt
eefd5bc2c547bf82b177b6259c13f7723dc876d9 target.txt
となっていて、確かに、SHA-1のハッシュ値と一致しています。
sign2.sigのデコード結果を読み解く
一方、sign2.sigの方は、明らかに署名に入力した情報そのものです。そのものが見えるということは、ダイジェスト関数すらかけられていないということです。
openssl rsautl -sign の正体
この挙動から推測すると、openssl rsautl -sign
は、RSASSA-PKCS1-V1_5-SIGNの処理の T を直接ファイルとして与えたような動作をしているように見えます。
また、openssl rsautl -verify
は署名を公開鍵で平文化し、結果が以下のEMの構造をしていると解釈し、Tを出力するコマンドのようです。
EM = 0x00 || 0x01 || PS || 0x00 || T
iOS/Macの Security Frameworkとの関係
iOSの Security Frameworkでは SecKeyRawSign() や SecKeyRawVerify() 関数で電子署名を扱うことができます。その際、パディングのオプションとして以下を指定することができます。
- kSecPaddingNone
- No padding.
- kSecPaddingPKCS1
- PKCS1 padding.
- kSecPaddingPKCS1MD2
- Data to be signed is an MD2 hash.
- Standard ASN.1 padding is done, as well as PKCS1 padding of the underlying RSA operation. Used with SecKeyRawSign and SecKeyRawVerify only.
- kSecPaddingPKCS1MD5
- Data to be signed is an MD5 hash.
- Standard ASN.1 padding is done, as well as PKCS1 padding of the underlying RSA operation. Used with SecKeyRawSign and SecKeyRawVerify only.
- kSecPaddingPKCS1SHA1
- Data to be signed is a SHA1 hash.
- Standard ASN.1 padding will be done, as well as PKCS1 padding of the underlying RSA operation. Used with SecKeyRawSign and SecKeyRawVerify only.
※ 公式サイトから引用
kSecPaddingPKCS1SHA1 を指定した場合
結論から言うと、kSecPaddingPKCS1SHA1の指定は、 openssl dgst -sha1 -sign
コマンドと対応しています。つまり、
EM = 0x00 || 0x01 || PS || 0x00 || T
ただし、 T = A || H
A = (0x)30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14
のEMを秘密鍵で暗号化したものが署名として利用されます。この時、SecKeyRawSign()/SecKeyRawVerify()の入力は H です。PSやAは関数が勝手に処理してくれます。
kSecPaddingPKCS1 を指定した場合
この場合、上記 kSecPaddingPKCS1SHA1 の処理から Aに関する処理が行われなくなります。つまり、
EM = 0x00 || 0x01 || PS || 0x00 || T
のEMを秘密鍵で暗号化したものが署名として利用されるのは同じですが、SecKeyRawSign()/SecKeyRawVerify()の入力が生のTであると解釈され、Aを付与する処理が行われません。
この挙動は openssl rsautl -sign
や openssl rsautl -verify
の動作と一致しています。
kSecPaddingNone を指定した場合
ちゃんと試していませんが、この場合、PSに関する処理も行われず、SecKeyRawSign()/SecKeyRawVerify()の入力が EM自体であると解釈されるのだろうと思います。
なぜこんなバリエーションがあるのか?
参考サイトにリンクした StackOverFlowの回答によると、Aのパディングをバイパスする処理は、SSL/TLSの処理で必要になるんだそうです。この辺はこれ以上調べていないのでよくわかりません。
Javaの JCEとの関係
JCEの Signatureクラスの仕様によると、実装クラスは署名アルゴリズムとして、以下の3つは必ずサポートする必要があると書かれています。
- SHA1withDSA
- SHA1withRSA
- SHA256withRSA
このうち、SHA1withRSAというのが iOSの kSecPaddingPKCS1SHA1や、OpenSSLコマンドの openssl dgst -sha1 -sign
と対応しています。