前回、送信データの改ざんを検知する為、簡易的なセキュリティトークンである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 DSigやJWT(証明書チェーンはx5cヘッダキーを使用)といった方式で行います。
p12ファイルは電子ファイルなのでセキュリティトークンとしては弱く、現実世界ではICカードが主に使われる為、開発したシステムに正式に取り入れることは少ないかも知れません。
ただICカードはカードベンダに協力して貰わないと何種類も用意出来ませんし、証明書の期限切れなどのテストをする場合はp12ファイルで代替が効いたりします。電子署名という仕組みを勉強する際にもp12はうってつけです。
インターネットで全てが完了する時代に向けて「電子署名法」も変わっていくと思われますが、PKCS#12ファイルを使って基本を実装出来るようになっておけば知識の追従は楽になりそうです。