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
-

執筆者:

関連記事

Doxygenでspring-frameworkをドキュメント化

以前にspring-frameworkのソースリーディングが出来る環境をOpenGrokで作りました。One IT ThingOpenGrokをインストールしてソースリーディング環境を作るhttps: …

インターネット上に流れる通信をTSUBAMEで俯瞰する

目次1 TSUBAMEとは1.1 四半期ごとに統計を公開してくれる1.2 週間レポートもしてくれる TSUBAMEとは JPCERTコーディネーションセンター TSUBAME(インターネット定点観測シ …

開発用のRSA鍵ペア(バイナリ、テキスト)をopensslで作っておく

目次1 環境2 秘密鍵3 公開鍵4 まとめ 環境 CentOS7.6に付属のopenssl 1.0.2k。 $ cat /etc/redhat-release CentOS Linux release …

Javascriptでコンパスを作ってAndroid、iPhoneが向いている方角を特定

普段は意識しなくても、AndroidもiPhoneも自然データをデジタルに変換するセンサーの塊です。 加速度センサー重力センサージャイロセンサー地磁気センサー気圧センサー照度センサー温度センサー位置セ …

JUnitコードの自動生成も出来る古の神Eclipseプラグイン「CodeProAnalytix」は今でも使える

デスマーチPM「各社のPG進捗が遅れていてユニットテストを書く暇が全く無いんですよ、なにかいい手は有りませんかね・・・? 因みに本プロジェクトはカバレッジ100%で請け負っています!(キリッ)」 何故 …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯技術者として楽しく生きることを目指しています。デスマに負けず健康第一。