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
-

執筆者:

関連記事

Chrome76でシークレットモード非検知を実現出来なかった件の検証

2019/07/30にChrome76がリリースされました。 76では以前からGoogleが問題視していた「閲覧者がシークレットモードでみているかどうか運営側が分かってしまう」が解決されるはずでした。 …

Android+ChromeでlocalhostアクセスしてPCサーバへPort Forward(Fwdアプリ使用)

昨今はプライバシーの侵害防止、セキュリティ観点から、HTTPS環境下でないと使えないHTML5 APIが増えました。 反してAndroid7でオレオレ証明書に関する仕様が変わり、Android6だった …

StripeとJavaで単発Web決済を一通り流してみる

会社仕事でも個人ビジネスでも、商品やサービスの対価として利用者に課金方法を提供したい時があるかと思います。 最もメジャーな決済方法であるクレジットカードで課金してもらう仕組みを導入したいところですが、 …

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

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

mkcertとhttp-serverでHTTPS環境を作りAndroid(chrome)、iPhone(safari)から接続

簡単にパーフェクトなオレオレ証明書が作れるとgithub上で人気上昇中の「mkcert」。 GitHub  135 UsersFiloSottile/mkcerthttps://github …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯現役技術者を目指しています。健康第一。