One IT Thing

IT業界で飯を食う為の学習系雑記

java javascript security

Javascript(暗号化JSライブラリ「Forge」)とp12ファイルで署名値を作成、Javaで検証する

投稿日:

前回、送信データの改ざんを検知する為、簡易的なセキュリティトークンであるPKCS#12形式のファイルを作成しました。

今回はクライアントとサーバの二者間でデータが改ざんされていないことを確認する為、

  • ブラウザ(Javascript)で送信データの署名値を作る
  • Java(サーバ想定)で署名値を検証する

上図が可能か検証していきたいと思います。

ブラウザ単体でp12ファイルを使えるかどうか調べる

大抵の言語ではp12ファイルから秘密鍵、証明書(公開鍵)を取り出すコアAPIやライブラリが在りますがブラウザ上のjavascriptではどうなのでしょうか。

W3C標準の「Web Crypto API」ではp12ファイルは読み取れない

W3C仕様実装の「Web Crypto API」を使うとライブラリ無しで鍵ペアの生成、署名、検証をすることが出来ます。

ただ残念ながら、PKCS#12ファイルに入っている鍵や証明書にアクセスする機能はありません。

鍵と証明書がブラウザメモリに展開されている状態ならこのAPIで問題ありませんが、今回のニーズには適いません。

暗号化JSライブラリ「Forge」はp12読み取り以外も全てカバー

暗号化JSライブラリのForgeはRSA、証明書関連の操作を概ね網羅しています。

同名のライブラリが複数ある為か、npmリポジトリでは「node-forge」と言う名前で公開されています。

同じことが出来そうなJSライブラリと比較してみるとダウンロード数が段違いですね。ライセンスもBSD(3-Clause)とLGPLのデュアルで使いやすいです。

これを採用してブラウザ上でp12ファイルを使った署名値作成をしてみます。

Forgeでp12ファイルを使った署名値作成

一枚っぺらのindex.htmlで公式を見ながら実装してみます。

実装

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Document</title>
        <script src="https://unpkg.com/node-forge@0.7.0/dist/forge.min.js"></script>
    </head>

    <body>
        <input id="fileP12" type="file" />

        <script>
            // p12ファイル読み込み
            fileP12.addEventListener("change", e => {
                let p12File = e.target.files[0];
                var reader = new FileReader();
                reader.onload = function() {
                    // p12ファイルから秘密鍵と証明書を取り出し
                    let p12DataUrl = reader.result;
                    let p12B64 = p12DataUrl.split("base64,")[1];
                    let p12Der = forge.util.decode64(p12B64);
                    let p12Asn1 = forge.asn1.fromDer(p12Der);
                    let p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, "hogehoge");

                    // 証明書
                    let cert = p12.getBags({ bagType: forge.pki.oids.certBag })[
                        forge.pki.oids.certBag
                    ][0].cert;

                    // 秘密鍵
                    let privateKey = p12.getBags({
                        bagType: forge.pki.oids.pkcs8ShroudedKeyBag
                    })[forge.pki.oids.pkcs8ShroudedKeyBag][0].key;

                    // SHA256で署名対象データのハッシュ値を作成
                    const md = forge.md.sha256.create();
                    md.update("サーバに送信するデータ", "utf8");

                    // 秘密鍵でハッシュ値を暗号化 = 署名値
                    const signature = privateKey.sign(md);
                    console.log(forge.util.bytesToHex(signature));

                    // 証明書内の公開鍵で署名値を検証
                    const result = cert.publicKey.verify(
                        md.digest().bytes(),
                        signature
                    );
                    console.log(result);
                };

                reader.readAsDataURL(p12File);
            });
        </script>
    </body>
</html>

HTTPサーバ経由で動作を検証

ブラウザの暗号化機能はHTTPS環境かlocalhostのHTTPサーバ経由でないと動作しません。既存のHTTPS Webサーバに載せるか、npmが使えるならhttp-serverでも入れてローカルホストでHTTPサーバを起動します。

$ npm install -g http-server

// index.htmlが有るディレクトリで
$ http-server

実行結果

http://localhost:8080にアクセスし、前回作成したforTest.p12を読み込みます。ブラウザの開発者コンソール実行結果。

0f9204d63178e6098d4b9557cd4a06ecad293dee306a27f553b9dc6ef9b2187b27ba95a84d5cce697ccd19c54a24e5ca8e3069a38762fc318c50bba6e7f1c8c691b29d775a5a6ccc9069abf598b42cc9369359c81425f7efacb7ed51e101bdfed1d2d7d79cd47894363f4c0efb524081fbc47c8f2ecd51958339c9ad7592d7d991ee363199d858ab704f431b49aa6a74de898ea83909654d466e666caccd5621fd8739c20add6323ce5e95459d8c3000dbffbaadb13d3c3629e4af966ae3b5d2a0509661fa2d87081eac9451cc79b571824fdc2a21f752762457f59090ec5490e592ac36e9eac4b4171722429bd97193476ef4ab6b69d2e895a11c9bef5dcd78
true

署名値を16進文字列で出しましたがBase64でやりとりするのが一般的かと思います。公開鍵による署名値検証も成功しています。

・・・成功していますが、単一ホストで署名値の正しさを検証しても意味が無いので、次はForgeで生成した署名値をJavaで検証してみます。

Javaで署名値を検証

お好きなIDEで以下を実装。公開鍵をロードする為、プロジェクト直下にforTest.p12を置いておきます。

実装

import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.Enumeration;

import org.apache.commons.codec.binary.Hex;

public class Verifier {

	public static void main(String[] args) throws Exception {

		// 署名対象データ
		String origStr = "サーバに送信するデータ";

		// ブラウザで作られた署名値
		String sigVal = "0f9204d63178e6098d4b9557cd4a06ecad293dee306a27f553b9dc6ef9b2187b27ba95a84d5cce697ccd19c54a24e5ca8e3069a38762fc318c50bba6e7f1c8c691b29d775a5a6ccc9069abf598b42cc9369359c81425f7efacb7ed51e101bdfed1d2d7d79cd47894363f4c0efb524081fbc47c8f2ecd51958339c9ad7592d7d991ee363199d858ab704f431b49aa6a74de898ea83909654d466e666caccd5621fd8739c20add6323ce5e95459d8c3000dbffbaadb13d3c3629e4af966ae3b5d2a0509661fa2d87081eac9451cc79b571824fdc2a21f752762457f59090ec5490e592ac36e9eac4b4171722429bd97193476ef4ab6b69d2e895a11c9bef5dcd78";

		KeyStore p12 = KeyStore.getInstance("pkcs12");
		p12.load(new FileInputStream("forTest.p12"), "hogehoge".toCharArray());

		// 証明書から公開鍵をロード
		Enumeration<String> e = p12.aliases();
		String alias = e.nextElement();
		X509Certificate c = (X509Certificate) p12.getCertificate(alias);
		RSAPublicKey publicKey = (RSAPublicKey) c.getPublicKey();
		System.out.println(publicKey);

		// 署名対象データをバイナリに変換
		byte[] sigByte = Hex.decodeHex(sigVal);

		// 公開鍵で署名値を検証
		Signature verifier = Signature.getInstance("SHA256withRSA");
		verifier.initVerify(publicKey);
		verifier.update(origStr.getBytes("UTF-8"));
		boolean result = verifier.verify(sigByte);

		System.out.println("result = " + result);
	}
}

実行結果

検証OK。

Sun RSA public key, 2048 bits
  params: null
  modulus: 22797578518878508834657582475068854153791918807811354806407050027556026294254630004982901925730192205538017792578196691865126376196945720124814953010222234278761106655922725088823374778244105949197534849245512625873676610515571679579670252403314660711737088372501206219626175141117633475986384817628195163486567668707283416824769009029433276542431080254644135979061837820910925699239959574834931427053725384211216900975010487015926331856960401762498599986688367220359557242642632585796111269328188083415224069292123876829937018947896551359800972975444818584396555416606580839979716550512172304951623695716727023719539
  public exponent: 65537
result = true
  • 送信されたデータのハッシュ値
  • 署名値を公開鍵で復号して出来た送信前のハッシュ値

が等しく、送信されたデータが改ざんされていないことが分かりました。

試しにJavaソース上の「送信されたデータ」を変えてみると、署名値検証はfalseになり、受信時のデータが送信時と比較して改ざんされていることを検知出来ます。

まとめ

実際にシステム開発する際、署名値、証明書のクライアント-サーバ間におけるやり取りはXML DSigJWT(証明書チェーンはx5cヘッダキーを使用)といった方式で行います。

p12ファイルは電子ファイルなのでセキュリティトークンとしては弱く、現実世界ではICカードが主に使われる為、開発したシステムに正式に取り入れることは少ないかも知れません。

ただICカードはカードベンダに協力して貰わないと何種類も用意出来ませんし、証明書の期限切れなどのテストをする場合はp12ファイルで代替が効いたりします。電子署名という仕組みを勉強する際にもp12はうってつけです。

インターネットで全てが完了する時代に向けて「電子署名法」も変わっていくと思われますが、PKCS#12ファイルを使って基本を実装出来るようになっておけば知識の追従は楽になりそうです。

-java, javascript, security
-

執筆者:

関連記事

IVS対応フォント「IPAmj明朝」で使えるフォントをWeb上に一覧表示してみる(1)

目次1 IPAmj明朝フォントとは2 日本における漢字のコンピュータ表示課題3 サロゲートペアが対応策だが4 IVS(ideographic variation sequence) とは5 そこでIP …

Tomcat9をCentOS7&OpenJDK11で起動

CentOS 7.6にOpenJDK 11を以下の記事で入れている状態です。 One IT ThingCentOS7にOpenJDK11をインストール、alternatives後の再ログイン …

Maven環境別ビルド時、プロファイルの違いでdependencyを変える

MavenのResourceFilteringを使い、production、staging、developmentとかで環境別ビルドしている時、ある環境ビルドの時だけ特定のライブラリを追加する、をmv …

電子署名法で求められる「本人性」と「非改ざん性」の実現方法をざっくり理解する

2019年現在ではマイナンバーカードが流通し始め、インターネット上で公的な申請が完了できる体制が整いつつあります。 日本は古式ゆかしい申請制度がしっかりしているのでなかなか諸外国より電子化が進みません …

Jerseyで開発したRESTで任意リファラからのCORSアクセスを許可する

package jp.hoge.filter; import java.util.Properties; import javax.servlet.http.HttpServletRequest; i …

 

shingo.7k24
 

東京在勤、職歴20年越え中年ITエンジニアです。まだ開発現場で頑張っています。

19歳(1996年)から書き始めた個人日記が5,000日を超え、残りの人生は発信をして行きたいと思い、令和元日からこのサイトを開始しました。勉強と試行錯誤をしながら、自分が経験したIT関連情報を投稿しています。

私と同じく、今後IT業界で生計を立てて行きたいと考えている方や、技術共有したいけどフリーランスで孤独、といった方と一緒に成長、知識共有して行けたら楽しいな、と思っています。