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
-

執筆者:

関連記事

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

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

JavaでRSA暗号を使う際にCRYPTREC暗号リストに足元をすくわれる可能性を回避する

標準的な暗号しか使わないケースでもJavaでRSAを使う時はBouncyCastleを入れておいた方が無難、という話です。 “ECB”という文字列がプログラム中にあると監査に引 …

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

前回の続きです。One IT ThingIVS対応フォント「IPAmj明朝」で使えるフォントをWeb上に一覧表示してみる(1)https://one-it-thing.com/2098IPAmj明朝フ …

Spring&JSPの検証環境を作る

2019年現在、オワコン風潮の強いJSPですが使っているプロジェクトもまだまだあり、枯れた技術を好む官公庁系のプロジェクトでは根強いシェアを誇っています。実装検証をする為に環境を作る機会があったりする …

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

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

 

shingo.nakanishi
 

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