公開鍵署名の概要
公開鍵署名(Public Key Signature)は、デジタル署名の一種で、公開鍵暗号方式を用いた署名方法です。公開鍵署名は、電子メールのセキュリティ、ソフトウェアやファームウェアに対する署名、オンライン取引など、多くのセキュリティが必要なアプリケーションで広く使用されています。公開鍵署名は非常に汎用性の高い技術なので、幅広い局面で応用できるはずです。
公開鍵署名の主な利点は、認証性と否認不可性です。認証性は、メッセージが特定の送信者によって作成されたことを保証します。否認不可性は、送信者が後でメッセージを送信した事実を否定することができないことを意味します。
本稿では、公開鍵署名の基本的な仕組みから始まって実際のプログラム例までを具体的に解説します。プログラム例では、wolfSSL社の暗号ライブラリーwolfCryptを使ったC言語によるプログラムを紹介します。wolfSSL社のソフトウェアは、オープンソース版を社内技術評価などの目的で使うことができます。商用の製品に組み込む場合は商用ライセンスを必要とします。
公開鍵署名は「署名生成」と「署名検証」の二つのプロセスからできています。
1. 署名生成: 送信者はメッセージに対して秘密鍵を使用して署名を生成します。この署名はメッセージに固有で、メッセージが変更されると無効になります。
2. 署名検証: 受信者(または第三者)は公開鍵を使用して署名を検証します。公開鍵は広く配布されており、誰でもアクセスできます。
セキュリティプロトコルやファームウェア更新ではその安全性を確保するために、通信の相手、メッセージの内容や配布するファームウェアが本物であり改竄されていないこと(真正性)を保証する必要があります。そのために、通信情報や配布するファームウェアに対して署名生成用の鍵(秘密鍵)を使用して署名を生成します。受け取る側では、あらかじめ配布されている署名検証用の鍵(公開鍵)と受け取ったメッセージやファームウェアと署名を検証して正しいものであること(真正性)を検証します。
公開鍵署名にはRSA, DSA, ECDSA, EdDSAなどたくさんの方式が考案、実用化されていますが、ここの記事では現在特に広く利用されているRSAについて具体的はプログラム例まで含めて説明します。
RSA(Rivest-Shamir-Adleman)は大きな整数の素因数分解の困難さに基づいた暗号アルゴリズムで、公開鍵技術の当初から知られ現在も広く利用されています。一方、ECDSAは楕円曲線暗号(ECC)に基づいた暗号アルゴリズムで、同じ強度の暗号化を実現するのにRSAより短い鍵で実現できる特徴があります。例えばRSAの2048ビットの鍵で得られる暗号強度はECCでは概ね256ビットで得ることができるとされています。また、最近は計算量の最適化が進み、RSAより少ない計算量で同等の強度を達成することができるようになってきています。
RSAの標準は初期にはRSA Laboratoriesによって策定された公開鍵に関する一連の標準規定、PKCS(Public Key Cryptography Standards)の中でPKCS#1として規定されました。その後、NIST (National Institute of Standards and Technology)によるSP800-57、ISO/IEC18033-2のようにより中立的、国際的な標準としても採用されています。
ECDSAは当初NISTのSP800-56A、57として規定されましたが、その後ISO/IEC1488-3にも含まれて国政的な標準としても認知されています。
RSA署名検証
RSA署名、検証の仕組み
RSA署名のプロセスは、大きく署名生成フェーズと検証フェーズの二つに分かれます。また、事前準備として署名と検証に使用するRSA秘密鍵と公開鍵のペアを用意します。
図1にRSA署名と検証の流れを示します。
まず、署名フェーズについて、図に従って順を追って見ていきます。
メッセージダイジェスト:まず、任意サイズの署名対象メッセージの長さを一定にするために、署名対象メッセージに対するメッセージダイジェストを求めます。ダイジェストを求めるアルゴリズムとしてPKCS#1ではSHA2を使います。RSA鍵として2048ビットの鍵を利用する場合は通常SHA256を用います。これによって32バイトのダイジェストが得られます。
DERエンコード:次に、このダイジェスト値にASN1レコードヘッダーを追加してDERフォーマットに変換します。生のダイジェストレコードヘッダーの追加によって若干大きなバイナリデータとなります。
パディング生成:DERエンコードされたデータを元に、パディングを生成します。RSA署名の場合、署名検証の際に単にRSAによる復元処理が正しく行われたというだけでは、それが改ざんや捏造によるものでなく元の署名データであるということが保証できません。これを保証するために、署名データに対してパディング部分を付加して、元の署名データの偽造を防止します。
RSAのパディング方式としては元々PKCS#1 v1.5で定められたものが広く使われていましたが、最近はさらにPSS(Probabilistic Signature Scheme)と呼ばれる方法がPKCS#1 v2.1で定められ、広く使用されるようになってきています。この方式ではパディング中に乱数を加えることでさらに偽造が困難となり、セキュリティレベルを高めることができます。PSSパディングで生成した署名は、同一の秘密鍵と署名対象メッセージであっても、生成ごとに異なる署名値となります。
パディング生成によって、署名対象データはRSA鍵サイズと同じサイズとなります。例えばRSA2048ビット鍵の場合、256バイトとなります。
RSA署名:上記のパディング生成した署名対象データに対してRSA秘密鍵を使って署名を生成します。
次に、署名検証フェーズについて見ていきましょう。検証のためには、検証対象の署名とともに、元の署名対象メッセージと検証用の公開鍵を使います。
メッセージダイジェスト:署名フェーズの時と同様に署名対象メッセージのメッセージダイジェストを求め一定のサイズのデータに変換します。
DERエンコード: これも署名フェーズの時と同様に、ダイジェスト値をDERフォーマットに変換します。正しい署名対象メッセージならば、署名の際と同じ値のバイナリーデータとなるはずです。
RSA署名の復元:次に、検証対象の署名をRSA公開鍵で復元します。正しく復元できれば、結果はパディング付きの署名データが復元できるはずです。
DERエンコード抽出:パディングされたデータのパディングフォーマットの正当性をチェックするとともに、DERエンコードされた署名データを抽出します。
比較:この署名データと先ほどのDERエンコードデータを比較し、一致すれば正しい署名として検証されたことになります。
コマンドによる鍵の生成
ここからは、実際のRSA公開鍵署名と検証の手順について解説していきます。
まず、準備として署名と検証のための秘密鍵と公開鍵を生成します。この処理はプログラムで行うこともできますが、ファームウェア更新の場合、この部分はコマンドを利用する場合が多いので、ここではまずはOpenSSLコマンドを使用する場合を紹介します。あとのセクションでプログラムで署名を生成する場合について紹介します(署名のサンプルプログラム)。
まず、次のように秘密鍵を生成します。生成された秘密鍵ファイルには秘密鍵とともに対応する公開鍵の情報も格納されます。この例では、2048ビットの鍵を生成し、生成した鍵ペアはprivate_key.pemにPEMフォーマットで格納されます。
$ openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048
次に、生成した秘密鍵から公開鍵を取り出します。この例では、public_key.pemに公開鍵を出力します。
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
DERフォーマットの公開鍵を出力したい場合は下のようにオプション”-outform DER”を指定します。
$ openssl rsa -pubout -in private_key.pem -out public_key.der -outform DER
コマンドによる署名
次に、生成した秘密鍵を使って署名対象のファイルに対応する署名を生成します。下の例では、署名対象のfile.txt に対してSHA256とPKCS#1 v1.5パディングを使った署名をsignature.sigに生成します。
$ openssl dgst -sha256 -sign private_key.pem -out signature.sig file.txt
PKCS#1 PSSパディングの場合は、次のように ”-sigopt rsa_padding_mode:pss” を指定して署名を生成します。
$ openssl dgst -sha256 -sigopt rsa_padding_mode:pss -sign private_key.pem -out signature.sig file.txt
検証のサンプルプログラム
ここでは、署名検証のプログラムをwolfCryptを使って実現した簡単な例を見ていきます。プログラムはパディングとしてv15の場合とPSSの場合で基本的な流れは同じですが若干異なる部分があります。PSSの方が若干シンプルなのでそちらを先に見ることにします。
1)PKCS#1 v2.2(PSS)
図2にPKCS#1 PSSによる署名検証のサンプルプログラムの主要部分(verify関数)を示します。ここでは、簡単にするためデータ定義や返却値のエラーチェックなどは省略しています。動作可能なサンプルプログラムは本稿末尾の参照に掲載したレポジトリからダウンロードしてください。
Verify関数は、署名対象データ(msg)、署名(sig)、DERフォーマットの公開鍵(pub)とそれぞれのデータサイズをアーギュメントとして受け取ります。
int verify(byte *msg, int msg_size, byte *sig, int sig_size, byte *pub, int pub_size) { /* Message Hash */ ret = wc_InitSha256(&sha256); ret = wc_Sha256Update(&sha256, msg_p, msg_size); ret = wc_Sha256Final(&sha256, msgd); /* Set up public key */ ret = wc_InitRsaKey(&rsaKey, NULL); ret = wc_RsaPublicKeyDecode(pub, &idx, &rsaKey, pub_size); /* Verify Sigature */ ret = wc_RsaPSS_VerifyCheckInline(sig, sig_size, &decSig, msgd, WC_SHA256_DIGEST_SIZE, WC_HASH_TYPE_SHA256, WC_MGF1SHA256, &rsaKey); return 0; }
verify関数では、まず受け取った署名対象メッセージのメッセージダイジェスト(sha256)を求めます。メッセージが長くていっぺんにメモリーバッファーに格納できないような場合は適当なサイズに区切ってバッファーに格納し、wc_Sha256Updateを必要回数繰り返すことも可能です。最後にwc_Sha256Finalでダイジェスト値を出力します。
次に、RSA鍵管理エリア(rsaKey)をwc_InitRsaKeyで初期化し、wc_RsaPublicKeyDecodeでDERフォーマットの公開鍵を読み込みます。この鍵と先ほどのダイジェストを指定してwc_RsaPSS_VerifyCheckInlineを呼び出し署名検証を行います。このサンプルでは、ダイジェストとパディングにSHA256を指定しています。
この関数ではDERエンコードやPSSパディングの処理を行った上で署名値を比較検証します。結果が正常であれば値ゼロを返却し、不正の場合はエラーの内容によって負の値の整数を返却します。
2)PKCS#1 v1.5
次にPKCS#1 v1.5パディングの場合を見ていきます。プログラムの全体的な流れは先ほどと変わらないのですが、署名検証には、先ほどのwc_RsaPSS_VerifyCheck関数の代わりに、v1.5パディング用のwc_RsaSSL_VerifyInlineを使います。この関数では、DERエンコードの処理を行わないので、verify 関数ではメッセージダイジェストを求めた後、ASN1によるDERエンコード処理を行います(wc_EncodeSignature)。wc_RsaSSL_VerifyInlineでは、DERエンコードしたものを検証対象のダイジェストとして指定します。
また、この関数ではダイジェスト値の検証を行わないので、memcmpで二つの値が一致するか検証します。値が等しければ署名は正しいと判断します。
int verify(byte *msg, int msg_size, byte *sig, int sig_size, byte *pub, int pub_size) { /* Message Hash */ ret = wc_InitSha256(&sha256); ret = wc_Sha256Update(&sha256, msg_p, msg_size); ret = wc_Sha256Final(&sha256, msgd); ret = wc_EncodeSignature(asnHash, msgd, 32, SHA256h); /* Set up public key */ ret = wc_InitRsaKey(&rsaKey, NULL); ret = wc_RsaPublicKeyDecode(pub, &idx, &rsaKey, pub_size); /* Verify Signature */ ret = wc_RsaSSL_VerifyInline(sig, sig_size, &decSig, &rsaKey); if (memcmp(decSig, asnHash, WC_SHA256_DIGEST_SIZE) != 0) { printf("Verify Failed: \n"); } else { printf("\nVerified\n"); } return 0; }
ノンブロッキングプログラム
組み込みシステムのファームウェア更新プログラムのようなものは、ブートプログラムの一部としてベアメタル(OS無し)で動作します。このような場合に、署名検証のような時間のかかる処理の途中で、他の優先度の高い処理も進めたいというようなことがあります。そうした場合にはwc_RsaSSL_VerifyInlineのような公開鍵処理を処理の途中で中断して関数から戻り、他の処理を呼べるようにすることができます。途中で中断した処理は再び同じアーギュメントを指定してwc_RsaSSL_VerifyInlineを呼び再開することができます。このような処理モードをノンブロッキングモードと呼びます。
RsaNb nb_ctx; ret = wc_RsaSetNonBlock(&rsaKey, &nb_ctx); do { ret = wc_RsaSSL_VerifyInline(sig, sig_size, &decSig, &rsaKey); •/* ここに他の処理を挿入 */ } while (ret == FP_WOULDBLOCK);
ノンブロッキングモードでもプログラムの全体構造は大きく変わることはありません。図4はwc_RsaSSL_VerifyInlineをノンブロッキングモードで動作させるための変更点を示しています。データエリアとしてノンブロッキング処理のための管理エリアRsaNbを定義し、wc_RsaSetNonBlockでRsaKeyと紐付けてこの鍵での処理モードとしてノンブロッキングモードを指定します。ノンブロッキングモードで動作するwc_RsaSSL_VerifyInlineは、適当なタイミングで処理を中断して関数呼び出しから戻ります。その時、擬似的なエラーコードとしてFP_WOULDBLOCを返却するので、これが返却された場合は、他に並列に実行したい処理を終えたのちに再びwc_RsaSSL_VerifyInlineを同じアーギュメントで呼び出します。処理がすべて終了した場合は、返却値ゼロ(正常終了)または負の値(処理エラー)を返却するので、その場合は繰り返し呼び出しを終了します。
署名のサンプルプログラム
“コマンドによる署名” ではOpenSSLコマンドで署名を生成する方法を紹介しました。ここでは、プログラムでwolfCryptのAPIを使って署名を生成する方法を紹介します。このプログラムは ” 図1. RSA署名と検証の流れ” の署名部分の処理を行います。
署名用の鍵ペア生成プログラム
署名処理を行う前に、まず署名と検証用の鍵ペアを生成します。このプログラムでは、生成した秘密鍵、公開鍵をDERフォーマットでそれぞれ指定されたファイルに出力します。
int keygen(FILE *fpri, FILE *fpub) { wc_InitRng(&rng); if(wc_InitRsaKey(&rsaKey, NULL); if (wc_MakeRsaKey(&rsaKey, 2048, 0x10001, &rng) priSz = wc_RsaKeyToDer(&rsaKey, pri, DER_SIZE) pubSz= wc_RsaKeyToPublicDer(&rsaKey, pub, DER_SIZE) fwrite(pri, priSz, 1, fpri); fwrite(pub, pubSz, 1, fpub); wc_FreeRng(&rng); }
RSA PSS署名プログラム
このプログラムではRSA PSSによる署名を実行します。
プログラムの前半はメッセージダイジェストを求める処理です。検証と同様にSHA256を使用します。
次に署名に必要なRSA鍵(rsaKey)と乱数(rng)を初期設定します。署名は “wc_RsaPSS_Sign” で実行しますが、このAPIで、DERエンコード、PSSパディングの生成とRSA署名の処理を行い、生成した署名をアーギュメントで指定されたバッファに返却します。正常に署名が完了した場合は関数返却値として署名サイズを返却します。
int sign(byte *msg, int msg_size, byte *sig, word32 *sig_size, byte *pri, int pri_size) { /* Message Hash */ ret = wc_InitSha256(&sha256); for (msg_p = msg; msg_size >= MSG_BLOCK; msg_size -= MSG_BLOCK, msg_p += MSG_BLOCK) { ret = wc_Sha256Update(&sha256, msg_p, MSG_BLOCK); } if (msg_size > 0) { ret = wc_Sha256Update(&sha256, msg_p, MSG_BLOCK); } ret = wc_Sha256Final(&sha256, msgd); ret = wc_InitRsaKey(&rsaKey, NULL); ret = wc_InitRng(&rng); ret = wc_RsaSetRNG(&rsaKey, &rng); idx = 0; ret = wc_RsaPrivateKeyDecode(pri, &idx, &rsaKey, pri_size); ret = wc_RsaPSS_Sign(msgd, WC_SHA256_DIGEST_SIZE, sig, sizeof(sig), WC_HASH_TYPE_SHA256, WC_MGF1SHA256, &rsaKey, &rng); wc_FreeRng(&rng); wc_FreeRsaKey(&rsaKey); return 0; }
まとめ
本稿では、公開鍵署名と検証について具体的なプログラム、コマンドの使い方を紹介しました。サンプルプログラムはRSA署名を使用していますが、ECDSAやEd25519などの署名検証でも、ほぼ同様の考え方でプログラムを作成することができます。詳しくはwolfSSL/wolfCryptユーザマニュアル( https://wolfssl.jp/docs/ )などを参照してください。
参照:本稿で紹介したサンプルプログラムの実行可能なフルバージョンとMakefileは下記のリンクからダウンロードできます。
RSA鍵生成、署名生成:
https://github.com/wolfssl-jp/examples/tree/master/aps/sign
RSA署名検証:
https://github.com/wolfssl-jp/examples/tree/master/aps/verify