前回は、 W3C Web Cryptography APIについて、一番簡単そうなハッシュの生成を紹介しながら、 幾つかの課題について紹介しました。
今回は、W3C Web Cryptography APIによる、 RSA公開鍵暗号によるデジタル署名の生成と検証を紹介していこうと思います。
署名と検証の例題って、よく一人の人というかプログラムが、 鍵ペアを生成して、署名して、検証するサンプルを出しますよね。( sjclの楕円暗号のテストコードとか・・・) 一人の人が同時に署名と検証を行うってユースケースとしてありえないと思うんですよね。 実際、私が見た当時、sjclでは鍵オブジェクトの秘密鍵と公開鍵を 分けてエクスポートができないようで、実際問題使えないことがありました。
そんなわけで、今回はまず、ちゃんとOpenSSLで生成した秘密鍵と公開鍵をインポートして、 署名の生成と検証をできるようにしたいと思います。 その後で、RSAの鍵ペアの生成をします。
データの準備
W3C Web Cryptoで鍵生成してもよいのですが、今回はOpenSSLで作ったRSA 2048bitの 秘密鍵で署名することにしたいと思います。PKCS#5 PEM形式の2048bit RSA秘密鍵を生成し、 それを平文のPKCS#8 DER形式に変換し、 bin2hexスクリプト で16進数文字列表現に変換します。
鍵ペアとなる公開鍵について、同様に16進数文字列形式のものを取得します。% openssl genrsa 2048 > k2048.p5p.pem % openssl pkcs8 -topk8 -nocrypt -in k2048.p5p.pem -outform DER -out k2048.p8p.der % bin2hex < k2048.p8p.der > k2048.p8p.hex #16進数形式のRSA秘密鍵
次に"aaa"という文字列に対して署名するとして、この文字列が含まれる署名対象 データファイルを作っておきます。% openssl rsa -in k2048.p5p.pem -pubout -out k2048.pub.pem # PEM形式の公開鍵 % openssl rsa -in k2048.p5p.pem -pubout -outform DER -out k2048.pub.der #DER形式の公開鍵 % bin2hex < k2048.pub.der > k2048.pub.hex # 16進数形式のRSA公開鍵
OpenSSLで前述の秘密鍵と署名対象データを使ってSHA1で署名すると結果のデータは以下のように 作られます。同様に16進数データを作っておきます。% echo -n aaa > aaa.txt
OpenSSLで前述の公開鍵と署名対象データと生成された署名値データを使って検証するには 以下のように行います。% openssl dgst -sha -sign k2048.p5p.pem aaa.txt > k2048oaaa.sig.bin % bin2hex < k2048oaaa.sig.bin > k2048oaaa.sig.hex
署名値は正しいことがわかります。% openssl dgst -sha1 -verify k2048.pub.pem -signature k2048oaaa.sig.bin aaa.txt Verified OK
秘密鍵のインポートと署名の生成
秘密鍵の16進数値を使って文字列"aaa"に対してSHA1withRSAで署名する場合の、 W3C Web Cryptoのコードは以下のようになります。
実装で注意しなければいけないポイントは以下の通りです。var prvHex = "308204bd02010030..."; // 前節の16進数秘密鍵 var prvUint8a = hextouint8a(prvHex); // 秘密鍵のUint8Array var aaaUint8a = asciitouint8a("aaa"); // 署名対象aaaのUint8Array // PKCS8形式の秘密鍵を署名用にインポート window.crypto.subtle.importKey( "pkcs8", prvUint8a, { name: "RSASSA-PKCS1-v1_5", hash: {name: "SHA-1"} }, true, ["sign"] ).then( // 秘密鍵インポートに成功したら署名する function(prvKey) { console.log("**importKey** 成功"); console.log("prvkey=" + prvKey); return window.crypto.subtle.sign("RSASSA-PKCS1-v1_5", prvKey, aaaUint8a); }, function(e) { console.log("**importKey** エラー: " + e); } ).then( // 署名に成功したら署名値(ArrayBuffer)を16進数表示 function(sigVal) { console.log("**sign** 成功"); console.log("sigVal=" + abtohex(sigVal)); }, function(e) { console.log("**sign** エラー: " + e); } );
- OpenSSLの(平文の)PKCS#8秘密鍵を使うにはimportKeyで"pkcs8"を指定する。
- 鍵使用目的で["sign"]を指定する。
hextouint8aについてはこちらをご覧ください。署名値16進数をk2048waaa.sig.hexとして保存 % hex2bin < k2048waaa.sig.hex > k2048waaa.sig.bin openssl dgst -sha1 -verify k2048.pub.pem -signature k2048waaa.sig.bin aaa.txt Verified OK
公開鍵のインポートと署名の検証
前々節のOpenSSLで作った署名値でもよいですし、前節で生成された署名値でも よいですが、これをインポートした公開鍵で検証してみましょう。 以下のようなコードで検証することができます。
実装で注意するポイントは以下の通りです。var pubHex = "30820122300d0609..."; // 前々節の16進公開鍵 var sigHex = "afd36b6f3f2af788..."; // 前節or前々節の16進署名値 var pubUint8a = hextouint8a(pubHex); // var sigUint8a = hextouint8a(sigHex); var aaaUint8a = asciitouint8a("aaa"); window.crypto.subtle.importKey( "spki", pubUint8a, { name: "RSASSA-PKCS1-v1_5", hash: {name: "SHA-1"} }, true, ["verify"] ).then( function(pubKey) { console.log("**importKey** 成功"); console.log("pubKey=" + pubKey); return window.crypto.subtle.verify("RSASSA-PKCS1-v1_5", pubKey, sigUint8a, aaaUint8a); }, function(e) { console.log("**importKey** エラー: " + e); } ).then( function(isValid) { console.log("**verify** 処理成功"); if (isValid == true) { console.log("**verify** 署名検証成功(一致)"); } else { console.log("**verify** 署名検証失敗(不一致)"); } }, function(e) { console.log("**verify** エラー: " + e); } );
- OpenSSLの公開鍵をインポートする際には"spki"を指定する。
- インポートする際の鍵使用目的に"verify"を指定する。
- 署名検証の結果はブール値(isValid)で返されるのでこれに従う。 関数が呼ばれただけで安心して終わりにしない。
RSA鍵ペアの生成
上の例ではインポートした秘密鍵、公開鍵を使っていますが、 鍵ペアの生成だってできます。(現状RSAだけのようですが・・・) RSA 2048bitの鍵ペアは以下のように生成します。
var paramKeyGen = { name: "RSASSA-PKCS1-v1_5", // 現状ではRSA-PSS, RSA-OAEPは指定できなそう modulusLength: 2048, // 鍵長 publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 公開指数 65537 hash: { name: "SHA-256" } // 署名のハッシュアルゴリズム?別に利用外のを指定しても良い? }; window.crypto.subtle.generateKey( paramKeyGen, // 生成する鍵ペアのための鍵長等の各種パラメータ true, // エクスポート可能にするかどうかのフラグ ["sign", "verify"] // 鍵使用目的 ).then( function(key) { console.log("**generateKey** 成功"); console.log("秘密鍵:" + key.privateKey); console.log("公開鍵:" + key.publicKey); }, function(e) { console.log("**generateKey** エラー: " + e); } );
Web Crypto APIの困った所5:importKeyの融通の利かなさ
こうしてこのブログをすんなり見ていただくと、「W3C Web Cryptoって簡単じゃん」と 思われるかもしれませんが、このような動く例を見つけるまで、かなりの紆余曲折があり 時間がかかっています。importKeyの正しく動く例というのが、仕様に記載されておらず、 他の方のサンプルも動くものあり、動かないものあり、何が正しいのかよくわかりません。 importKeyの引数には、
- format - 鍵データの形式 (pkcs8, spki, jwk, raw)
- keyData - 鍵データ (ArrayBufferView(Uint8Array)かJSONデータ)
- algorithm - アルゴリズムのJSONデータ
- exportable - エクスポート可能かのブール値
- keyUsage - 鍵使用目的の配列 ["sign", "verify"] 等
- 鍵のデータ形式など指定させる必要があるのか。公開鍵か秘密鍵のバイナリかはASN.1構造を見ればわかる話。JWKだって区別はできる。
- アルゴリズムの指定も必要性がよくわからない。pkcs8、spkiであれば、鍵アルゴリズムが何であるかわかるし、鍵のインポートの際に署名アルゴリズムやハッシュアルゴリズムを指定させる 意味がわからない。"RSASSA-PKCS1-v1_5"にするか"RSA-PSS"にするかは、鍵インポート時に決める必要がない。署名のハッシュアルゴリズムについても同様に決める必要がないのに、"MD-5"などサポート外のアルゴリズムを指定するとエラーとなる。JWKデータも同様に鍵アルゴリズムの指定の必要がない。
- keyUsageの指定の必要性もよくわからない。例えば、RSASSA-PKCS1-v1_5で秘密鍵をインポートしたらkeyUsageはsignに決まっており、省略できることの方が多い。
- JSON等指定の自由度が非常に高い割に、値の指定を間違えるとすぐにエラーとなり融通が利かない。省略可能やデフォルト値を持つ引数、パラメータがあっても良さそうだが、そのようにはなっていない。
- algorithmのhashのパラメータ値でMozillaのテストコードでは"SHA-1"となっているが、仕様上は{name: "SHA-1"}となるのが正しいようでこれならChromeでもFirefoxでも動作する。前述のように必要の無いパラメータの指定方法が原因で実装により動作するもの、しないものがあり、相互運用性の問題が生じている。
Web Crypto APIの困った所6:アルゴリズムサポート状況の不明瞭
前回紹介したCan I Use CryptographyのページでChrome等いろんなブラウザが フルサポートかのような記述になっていますが、W3Cの勧告候補でも特にどのアルゴリズムは 実装必須となっているわけではないようで、ECDSA、RSA-PSSなど サポートしていないアルゴリズムはかなり多いです。(IE11+ではSHA-1未サポートのよう) この辺りについてスペックがどうなっているのかブラウザベンダーから正式な開示が 無いようですし、仕様上も何をサポートしているかを知る術が特になく、 動かしてみて動かなかったら未サポートのような状況になっています。 そもそも、Java JCEやOpenSSLなどに比べたら、 アルゴリズムの数が圧倒的に少ないのですから、全アルゴリズムを実装必須(MUST) としても良いぐらいではないかと思います。 また、Java JCEではどのようなアルゴリズムをサポートしているかを知るためのAPIがあります。 ブラウザ毎のアルゴリズムや機能のサポート状況の比較は、 そのうち表などで比較できればと思っています。
Web Crypto APIの困った所7:サポートアルゴリズムの狭さと変なバランス感
W3C Web Crypto APIでは、 MD5、RSA暗号(RSAES-PKCS1-v1_5)など現時点で脆弱とされているアルゴリズムは除外されており、 サポートされているアルゴリズムはかなり限定的で狭いものです。 後方互換性や相互運用性のために使いたいというケースもあるでしょうから、暗号ライブラリとしては、 サポートしても良いのかなと思ったりもします。 その割には、例えばFirefoxではRSA鍵の鍵長が256bit〜7168bitをサポートしており、むしろそちらの方を制限すべきなのでは?とも思ってしまいます。 また、FirefoxではECDSAをサポートしていないのに、ECDHはサポートしているなど、同じECCの鍵生成なのに、このあたりのバランス感や優先順位も奇妙に思います。
Web Crypto APIの困った所8:鍵生成のパラメータ
例えばRSA鍵生成のパラメータですが、RSA署名やRSA暗号化のアルゴリズムを 指定しなければならないのかわからず、公開指数の指定の仕方も面倒で、 何故ハッシュを指定しなければならないのかわかりません。 また、これらのパラメータは省略は一切許されていません。 間違えるとエラーとなり、どこが間違っているのかに関するエラーメッセージは どの実装も非常に不親切です。
var paramKeyGen = { name: "RSASSA-PKCS1-v1_5", // RSA-PSS, RSA-OAEPなど指定する必要なくRSAで十分では? modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 整数65537でいいのでは?Uint8Array面倒 hash: { name: "SHA-256" } // hashが必須な意味がわからない };
高い相互運用性のために
一連のシリーズでは、最も汎用性が高くなるように、 window.crypto、window.crypto.subtleを使っており、 FirefoxやChromeでそのまま動く動作コードになっています。 これらに加え、IEやSafariなどいろんなブラウザで動かそうとする場合には、 以下のようなコード先頭に入れ、
window.cryptoの代わりにWCを、 window.crypto.subtleの代わりにWCSを使えば、 どこでも動く可能性が高くなるかと思います。var WC = null; var WCS = null; if (window.msCrypto) WC = window.msCrypto; // IE11+ if (window.crypto) WC = window.crypto; // FF34+,CH37+ and others if (WC.subtle) WCS = WC.subtle; // IE11+,FF34+,CH37+ and others if (WC.webkitSubtle) WCS = WC.webkitSubtle; // Safari 7.1+
おわりに
今回は、RSA鍵ペアの生成、署名生成、署名検証についてみてみました。 今日はこんなとこで。
そこでご質問なのですが、
今の一番の悩みどころというのが、「電子署名に用いる秘密鍵をどう得るのか」、です。
この記事ではopensslで秘密鍵をGETするサンプルとなっていますが、
実運用において、クライアント証明書の秘密鍵はエンドユーザのみが知り得るべき情報であり、
秘密鍵に関する処理はサーバサイドで行うわけにはいかないと考えています。
例えば既にパブリック認証局の証明書を持っているエンドユーザに対して、
電子署名のWebアプリケーションを考える場合、リッチクライアントやActiveXを用いればもちろん実現は可能だと思いますが、
Webブラウザ単体の機能で何とか電子署名を行えないものでしょうか。
W3C Web Cryptography APIならできるのかな?と期待しています。