NodeのRSA鍵生成・署名・検証について速度比較してみた
Node.JSには、RSA鍵生成、署名、署名検証について、標準モジュールや、流行りのWebAssemblyを使った方法や、怪しいライブラリを使った方法、、、(それ、ワシのやないかぃ!!!)などありますが、、、年末年始ちょっと体調が悪くて家に籠もっていたのでちょっと速度比較してみました。(ちょっとじゃ済まなかった、、、)
特に、私のJavaScriptのPKIライブラリ jsrsasign はTom WuさんのRSA暗号のライブラリを使っているわけですが、「おせ〜」のやいの言われるので、どれくらい「おせ〜」のか興味があったり、流行りのWebAssemblyを使えばマシになるのか、、、とか調べてみたかったわけです。
比較対象となるRSA署名を扱う方法4種
Node.JSには標準でバンドルされている方法や、そうでないものがあり、今回速度比較したのはこの4つです。
標準のcryptoモジュール
cryptoモジュールはNode.JSに標準でバンドルされているOpenSSLベースのモジュールです。
標準のW3C Web Cryptography APIモジュール
webcryptoモジュールはNode.JSに標準でバンドルされている、W3Cで標準化されたW3C Web Cryptography APIの実装です。多分中ではOpenSSL使っていると思います。
jsrsasign
jsrsasignは、拙作の暗号やPKIに関するライブラリでnpm公開もしています。RSA暗号の部分はTom WuさんのRSA暗号のライブラリを使っており、これを拡張してRSA署名の機能を追加しています。
wasm-rsa
流行りのWebAssembly、WASMの波に乗り遅れるな!!!(とっくに乗り遅れてた)ってことで、
wasm-rsaモジュールは、npmパッケージとして公開されているモジュールだそうです。Rustの暗号ライブラリをベースにwasm化しているようです。jsrsasignが「おせ〜」と文句を言われるので、引越し先、共存の候補として調べてみました。
調査内容
以下の3つの機能について処理速度を測定します
- 1024、2048、3072、4096ビットのPEM形式のRSA 鍵ペアの生成
- 1024、2048、3072、4096ビットのPEM RSA秘密鍵による署名
- 1024、2048、3072、4096ビットのPEM RSA公開鍵による署名検証
調査条件
方法による条件の差がおきないように以下の条件で測定しました。
- 処理の入出力は共通化します。
- 外部の鍵を使う場合には、PEM形式の秘密鍵(PKCS#8)、公開鍵(SPKI)の文字列を入力とします
- 署名対象は"aaa"の文字列とします。これにより、ハッシュ対象データの大きさの影響は少なくなります。
- 署名値の入出力は16進数文字列とします。
- 対象のモジュールの提供する機能で入出力の条件を満たせない場合には、jsrsasign等の関数でこれを補い変換します。
- 測定マシン、OS、処理系は同じものを使用します。
- WebCryptoAPIなど、非同期APIは同期させ逐次実行するようにします。
- 測定は3セット測定した平均を取り、1セットは処理が概ね30〜1分の間になるように処理回数の調整を行います。
- 処理時間の測定はprocess.hrtimeを使います。
- 1分間の平均処理回数で比較します。
測定環境
全てのテストを以下の環境で測定しました。
- マシン: MacBook Air (M1, 2020)
- メモリ: 16GB
- OS: macOS Big Sur 11.6
- 処理系: Node.JS 17.2.0
- モジュール
- wasm-rsa 0.3.3
- jsrsasign 10.5.1
RSA鍵ペア生成(分間平均生成数)
4つの方法、4つの鍵長で測定した鍵ペアの生成速度です。
RSA 1024 | RSA 2048 | RSA 3072 | RSA 4096 | |
---|---|---|---|---|
crypto | 12588.76 | 1551.27 | 423.76 | 166.55 |
webcrypto | 12097.81 | 1563.69 | 394.90 | 153.46 |
jsrsasign | 403.80 | 28.49 | 13.66 | 2.61 |
wasm-rsa | 224.66 | 20.41 | 3.31 | 1.18 |
webcryptoは、cryptoをベースにしており、同様にOpenSSLベースで処理をしていると思いますが、webcryptoを使うと若干落ちるようです。また、jsrsasignも、wasm-rsaも同様に組み込み関数を使わずにPure JavaScript実装なのでかなり遅いわけですが、wasm-rsaに比べてjsrsasignの方が早いようです。
RSA署名
4つの方法、4つの鍵長で測定したSHA256withRSA署名を行った署名速度です。
RSA 1024 | RSA 2048 | RSA 3072 | RSA 4096 | |
---|---|---|---|---|
crypto | 86007.62 | 44879.46 | 22519.33 | 12208.72 |
webcrypto | 73218.41 | 39206.84 | 20506.96 | 11418.29 |
jsrsasign | 22662.15 | 3118.06 | 946.36 | 414.40 |
wasm-rsa | 3988.51 | 529.05 | 159.74 | 67.97 |
同様にwasm-rsaについてはちょっと問題があるんですが、それは後述します。
RSA署名検証
4つの方法、4つの鍵長で測定したSHA256withRSA署名を検証した検証速度です。
RSA 1024 | RSA 2048 | RSA 3072 | RSA 4096 | |
---|---|---|---|---|
crypto | 192046.68 | 181387.74 | 171308.33 | 158719.51 |
webcrypto | 164646.15 | 149275.64 | 134505.51 | 121880.43 |
jsrsasign | 346774.22 | 100850.20 | 46063.86 | 26543.13 |
wasm-rsa | 56843.02 | 14738.55 | 6606.45 | 3734.35 |
同様にwasm-rsaについてはちょっと問題があるんですが、それは後述します。
モジュールによる大まかな速度比較まとめ
各処理について鍵長で平均をとり、cryptoの処理速度を100.0としたときの、他のモジュールの速度比を表にまとめました。
鍵生成 | 署名 | 署名検証 | 処理平均 | |
---|---|---|---|---|
crypto | 100.0 | 100.0 | 100.0 | 100.0 |
webcrypto | 95.6 | 89.3 | 80.8 | 88.6 |
jsrsasign | 2.5 | 10.2 | 70.0 | 27.6 |
wasm-rsa | 1.8 | 1.8 | 11.0 | 4.9 |
測定からわかったこと
- とあるwasmのセミナーで「wasmは遅いわけではないが凄く速くなるというわけでもない」というような旨を仰っていた講師の方がいましたが、wasm-rsaに関しては、OpenSSLネイティブベースとの比較はもちろん、jsrsasignよりも遅くなってしまうことがわかった。
- 速度の順は crypto > webcrypto >> jsrsasign > wasm-rsa となる
- jsrsasignもwasm-rsaも、特に鍵生成が遅い
- jsrsasignのRSA署名生成は少し許せる、署名検証に関しては割と許せる範囲ではないか
- webcryptoは、cryptoより1割程度性能が落ちる
- OpenSSLベースのcryptoとwebcryptoは特に鍵長が大きくなってもそれほどパフォーマンスが落ちない。jsrsasignとwasm-rsaは鍵長が大きくなるとパフォーマンスがガクッと落ちる
- 組み込みモジュールを使わないでPure JavaScriptでという制限で、jsrsasignとwasm-rsaを比較するとそれほどwasm-rsaではパフォーマンスが出ず、jsrsasignでwasm-rsaを使うというメリットは無さそう
ちょっと考察
- crypto、webcryptoの署名や署名検証において、鍵長が小さい時に特にパフォーマンスが上がらない件で少し見てみた所、PEM鍵のインポートで非常に時間がかかっているようで、その実装があまり良くないのかもしれない。jsrsasignではPEM鍵利用のオーバーヘッドは非常に少ないようだ。
今後の予定
本当は楕円もやってみようとしてて、secp256k1曲線に限定されちゃいますけど、敬愛するherumiさんのecdsa-wasmも含めて比較してみようとしたんですが、鍵のPEMインポート、エクスポートをすることができず頓挫しています。内部の鍵表現がわかればいいんですけどね〜。ecdsa-wasmは確かC++ベースで書かれていると思うんですが、チョッパヤで多分、公開鍵暗号や署名に関してはRust WASMはダメダメでherumiさんのようにC++ WASMでないとだめなんじゃないかなぁ、、、と想像したりしています。うまくいくようならC/C++ベースで公開鍵暗号部分は作ってWASMにするのがいいのかなぁと思ったりします。
あと、ブラウザ上でのW3C Cryptography API、WASM、jsrsasignの比較もしてみたいんですが、まぁ、そのうちってことで。
(補足) wasm-rsaのバグについて
OpenSSLやjsrsasignで生成した署名が、どうもwasm-rsaで検証成功せず、また逆にwasm-rsaで生成した署名もまた、OpenSSLやjsrsasignで検証成功しなくて困ってたんですが、秘密鍵でエンコードしてDigestInfo構造を取り出してみると、、、
SEQUENCE
SEQUENCE
ObjectIdentifier (2 16 840 1 101 3 4 2 8) ←間違ってSHA3-256のOIDが入っている
NULL
OCTETSTRING 9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0
↑でもSHA256のハッシュ値を格納 (この例は文字列aaaのSHA256ハッシュ)
というわけで、wasm-rsaにはSHA3-258と実態の計算のSHA256が一致しないので、OpenSSLやjsrsasignで検証しようとすると検証エラーとなる署名を生成してしまうというバグを発見してしまいました。Rust素人なので自分で直すのは面倒だったので、とりあえずissueにあげておきました。
Discussion