自堕落な技術者の日記

基本は喰ってるか飲んでるかですが、よく趣味でカラオケ・PKI・署名・認証・プログラミング・情報セキュリティをやっています。旅好き。テレビ好きで芸能通

RSA署名

W3C Web Cryptography APIとの果てしなき戦い(第3回 動くサンプル1)

前回はW3C Web Cryptography APIの鍵生成、署名などについて説明しましたが、実際に動く実例がないとAPIの有り難みもわかないのかなと思いまして、とりあえず3つのサンプルを作ってみました。サンプルはChromeやFirefoxでは動くようになっています。画像をクリックするとサンプルページが開くようになっています。

ハッシュ関数


wc-hash
ハッシュアルゴリズムを選んで、ハッシュ計算したい文字列(UTF-8だから日本語も可能)を入力して、「ハッシュ計算」のボタンを押すだけで、16進数でハッシュ計算結果が表示されます。

公開鍵ペアの生成


wc-genkey
RSA公開鍵ペアを生成します。W3C Web Cryptography APIでは、鍵生成やインポートの際、どのような用途で、どのようなハッシュ関数と一緒に使用するかを指定する必要があるようで、生成時に選択するようにしていますが、PKCS#8 で秘密鍵や公開鍵をエクスポートする際には、まぁ、関係のない話になってしまいます。PKCS#8のバイナリデータ(ArrayBufferView)をPEM形式に変換するのにjsrsasignライブラリを使っています。

RSA鍵による署名と検証


wc-sigver
RSA鍵でUTF-8の文字列に対してSHA256withRSA署名アルゴリズムで署名と署名検証ができます。鍵はW3C Web Cryptography APIで内部生成した鍵ペアで署名と署名検証することもできますし、PKCS#8 PEM形式やJSON Web Key(JWK)フォーマットの秘密鍵や公開鍵をインポートして署名や署名検証することもできます。

サンプルの動作環境

これらのサンプルページは新しいChromeのWindows、Android、Mac OS X、Linux版、新しいFirefoxのWindows、Mac OS X、Android版で動作します。Safariでは一部がMac OS X版、iOS8+などで動作します。Internet ExplorerではPromise APIをサポートしていないので動作しません。IEでも動作するようにコード書くのはちょっと面倒で、それらの相互運用性を持たせるような実装の仕方については別途紹介しようかと思っています。

おわりに

とりあえず、いろんなブラウザでW3C Cryptography APIの動作を試してみることができるので、遊んでやってください。ソースコードはgithubに置いてあるのでご覧ください。FirefoxとChromeだけでも相互運用性持たせるのに結構壁にぶちあたったりして、その上、SafariやIEなども一緒にサポートするコードを書くのはかなり骨が折れるんですよね。でも、AndroidのFirefoxでも動くのを見ると、ちょっと目がうるうるしてきたりしますw。今日はこのへんで。

関連記事

W3C Web Cryptography APIとの果てしなき戦い(第2回 RSA署名生成と検証)

前回は、 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進数文字列表現に変換します。

% 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秘密鍵
鍵ペアとなる公開鍵について、同様に16進数文字列形式のものを取得します。
% 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公開鍵
次に"aaa"という文字列に対して署名するとして、この文字列が含まれる署名対象 データファイルを作っておきます。
% echo -n aaa > aaa.txt
OpenSSLで前述の秘密鍵と署名対象データを使ってSHA1で署名すると結果のデータは以下のように 作られます。同様に16進数データを作っておきます。
% openssl dgst -sha -sign k2048.p5p.pem aaa.txt > k2048oaaa.sig.bin % bin2hex < k2048oaaa.sig.bin > k2048oaaa.sig.hex
OpenSSLで前述の公開鍵と署名対象データと生成された署名値データを使って検証するには 以下のように行います。
% 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"]を指定する。
コンソールに"sigVal="で表示された16進数の署名値は、 k2048oaaa.sig.hexファイルの値と同じになっていると思います。 hex2binスクリプト で署名値をバイナリデータに変換して、以下のようにOpenSSLで検証することが できます。うまく検証できたでしょうか。
署名値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
hextouint8aについてはこちらをご覧ください。

公開鍵のインポートと署名の検証

前々節の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でも動作する。前述のように必要の無いパラメータの指定方法が原因で実装により動作するもの、しないものがあり、相互運用性の問題が生じている。
そのような意味では、jsrsasignのKEYUTIL.getKey()メソッドでは入力は極力自動解析し最小のパラメータで動作するようになっていて、秘密鍵であろうが、公開鍵であろうが、JSON形式であろうが、鍵が暗号化されていようが、鍵アルゴリズムが何であろうが、許容範囲の広い実装となっています。

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などいろんなブラウザで動かそうとする場合には、 以下のようなコード先頭に入れ、

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+
window.cryptoの代わりにWCを、 window.crypto.subtleの代わりにWCSを使えば、 どこでも動く可能性が高くなるかと思います。

おわりに

今回は、RSA鍵ペアの生成、署名生成、署名検証についてみてみました。 今日はこんなとこで。

関連記事

図説RSA署名の巻

RSA鍵による署名って、フツーはJava JCEでもCryptoAPIでも.NETでもOpenSSLでも、一つのオペレーションになっていて、中でどう処理されているから知る必要もないんですが、やろうと思えばハッシュとRSA鍵による暗号化・復号の暗号プリミティブで実装することはできます。

今回はRSA署名の中身を図説したものって、なかなか良い物が無かったので、ちょっと書いてみて、関連した相互運用上の問題なんかを紹介します。

RSA署名とは何?よ〜し父さん図説しちゃうぞ



RSA署名って、

文書のハッシュ取って秘密鍵で暗号化するんだよね


って簡単に解説しているものがあったりしますが、その説明って、なんか合っているようで合っていないというか、うそ臭いというか、「本当はどうなの?」っていうはなしが抜けているようで、これまでスッキリしませんでした。多分、それはパディングの話が無いからなんじゃないかと思うんです。

RSA署名の方式はPKCS#1 v2.1の中で定められているんですが、一般的な公開鍵証明書やCMS署名なんかは、その中で定められた "RSASSA-PKCS1-v1_5" というアルゴリズムを使っています。

RSASSA-PKCS1_v1_5アルゴリズムにかなり忠実に図説してみたのがコレ:

図1



左から右(→)が「署名の生成」で、右から左(←)が「署名の検証」です。

RSASSA-PKCS1-v1_5 のオペレーションは大きく、

・ハッシュの計算
・パディング処理
・公開鍵暗号

の3つに分かれます。DigestInfoを作るとこも含めてパディング処理と読んだりすることもあり、この図の方が間違っているかもしれませんが、一般的なプログラミング上のパディング処理を知っている方なら、DigestInfoを作った後からをパディング処理と呼んだ方がスッキリするようなが気がします。

DigestInfoと相互運用性



署名では、署名対象データのハッシュ値とハッシュアルゴリズムを格納するためにDigestInfoというASN.1構造を使います。

RSA PKCS#1 v2.1より
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-sha1 PARAMETERS NULL }|
{ OID id-sha256 PARAMETERS NULL }|
{ OID id-sha384 PARAMETERS NULL }|
{ OID id-sha512 PARAMETERS NULL }
}


署名生成時のアルゴリズムパラメータNULL



DigestInfoでハッシュアルゴリズムを指定する際に、アルゴリズムパラメータを指定できるんですが、SHA1、SHA2シリーズの場合にはNULLを指定します。このNULLを入れる入れないで、相互運用上の問題が起きたりします。

この辺りは昔、RSAとNISTの間で紆余曲折あったそうなんですが、解決のためにRSAはv2.1の訂正(PKCS #1 v2.1 Errat)を2005年12月に出しています。結論から言うと

・署名生成時:NULLを含めるものとする(SHALL)
・署名検証時:NULLがあってもなくても検証できるものとする(SHALL)

生成については、これまで自分が触ることができた10ぐらいの署名実装で一つを除き問題なくNULLが含まれていましたし、手当たり次第調べた証明書、CRLなんかも全てNULLが含まれた形になっています。残りの一つについても、パッチが出ているそうなので早く修正されるといいなぁ、、、と思います。(とある製品が国内2つの実サービスで使われているんですが、もう長いことパッチ未適用になってます。残念。)

検証については、目にした殆どの実装がNULLがあっても無くても検証できるようになっているんですが、入手できたうちの20%ぐらいの実装はNULLが無いと検証失敗します。

この辺りは、ハッシュとRSA暗号のプリミティブを使ってNULLの入ってない署名を作ったり、生の署名値、証明書、CMS署名、XML署名などで署名値のDigestInfoにNULLが入っているのかどうかをチェックするツールを作って調べました。

なんか、今回も細かい話ですみませんね。ではでは。
最新記事
Categories
Archives
Twitter
記事Google検索

本ブログ内をGoogle検索
Yahoo!アクセス解析
Travel Advisor
記事検索
QRコード
QRコード
  • ライブドアブログ