LoginSignup
29
35

More than 5 years have passed since last update.

クライアント証明書による認証の証明書を動的に変更するぞ

Posted at

前書き

この記事について

TIS Engineer Advent Calendar 2015 5日目の記事です。
いま絶賛開発中の「あるシステム」に対する真面目な相談が仕事で飛んできたのでそのネタで。
普段はネタがないのでエンタープライズなにそれみたいなノリで書くつもりだったけれど
エンタープライズな「あるシステム」でのクライアント証明書のお話です!

あるシステムについて

  • あるシステムにおいてデータ連携(HTTPS)を行う
  • 連携先はインターネット越しで連携元をクライアント証明書によって認証する
  • あるシステムはマルチテナント(1システムに複数の企業が乗ってる)で、アプリケーションは共用する。

課題

  • (あるシステムの)データ連携先は通信相手をクライアント証明書で識別する
  • 連携データの持ち主のクライアント証明書を選択して通信をする仕組みが必要

目的

  • 連携データに対応した証明書をJavaで動的に選択して利用するようにする。

ちなみに、クライアント証明書は通常持ってるものの中から、通信先に応じて適切に選ばれるようです。
しかし、「あるシステム」とデータ連携先は同じ通信先になるので、今回は通信先ではなく送信する内容に対応したクライアント証明書をアプリケーションから指定する必要があるということです。

目的を達成するまでの準備が多いので迷子にならないように!

この記事で実際にやること

事前準備

  1. (オレオレ)認証局を作成
  2. サーバ用のクライアント証明書作成
  3. クライアント証明書(First)の作成
  4. クライアント証明書(Second)の作成

前日の @Sheile4日目の記事で記載されてるように、オレオレ証明書の利用は良くないのですが、「クライアント証明書による認証」を正しく行ったとJavaに見せかける為に信頼できるルート証明機関としてオレオレ認証局を作成して利用します。(もちろん本番のシステム向けではなくこの記事の中だけの手順です。)
その為、この記事内ではオレオレ証明書を使っていますがそれに伴う警告はでてきません。

クライアントサイド(Java)

  • JVMで利用するkeystoreを作る
  • クライアント証明書を利用して接続
  • クライアント証明書に対応したパラメータをつけてHTTPリクエストを出す
  • サーバから200 OKが返ってるのを見る

サーバサイド(node.js)

  • クライアント認証を実施し、利用したクライアント証明書の情報をチェックする。
  • クライアントからのパラメータと証明書が対応することをチェックして、OK,NGを返す。

サーバサイドをnode.jsで書いた理由は趣味です。

実作業

事前準備作業

HTTPS Authorized Certs with Node.jsを参考にしながら証明書群を作成。
真面目にやると結構面倒なのでgithubのリポジトリ参照。
sample配下のcreate_cert.shで以下の内容を実施してます。

  1. 認証局、サーバ、クライアント1、クライアント2、計4つの秘密鍵を作る。
  2. 作成した認証局で、サーバ、クライアント1、クライアント2の証明書の作成依頼(CSR)をする。
  3. サーバ、クライアント1、クライアント2、計3つの証明書(CRT)を作成する。

サーバサイド(node.js)

クライアントからの接続に用いた証明書の組織と、GETパラメータに乗ってきた文字列を比較して一致したら200(デフォルト値)、一致しない場合406を返すサーバを用意します。
初めてES2015で書いたのでお作法悪いかも。

app.js
"use strict"
let https = require('https'),
fs    = require('fs'),
util  = require('util'),  
url   = require('url');

let httpsServ = https.createServer(
    {   
        requestCert: true,
        rejectUnauthorized: true,
        key:  fs.readFileSync ('./sample/server-key.pem'),
        cert: [fs.readFileSync('./sample/server-crt.pem')],// クライアント側で検証される証明書の設定                             
        ca:   [fs.readFileSync('./sample/ca-crt.pem')], // オレオレ認証局(検証用)                                             
    },
    (req, res) => {
        // Getパラメータで渡されるOrg                                                                                            
        let orgByParam = url.parse(req.url, true).query.o || '';
        // 実際の接続時に利用された証明書のOrg                                                                                   
        let cert = res.connection.getPeerCertificate();
        let orgByCert = cert.subject ? cert.subject.O : '';
//      res.write(util.format(cert)); // for debug                                                                               

        // 出力用文字列作成(console.logとresponse body)                                                                         
        let outText =
            "Param:" +orgByParam + '\n' +  
            "Org  :" +orgByCert + '\n';
        // 一致しない場合406                                                                                                     
        if(orgByParam !== orgByCert && orgByCert !== ''){  
            res.statusCode = 406 //Not Acceptable                                                                                
        }
        // 出力                                                                                                                  
        console.log(outText);
        res.write(outText);
        //res.write(util.format(cert)); // for debug                                                                             

        res.end();
    }  
).listen(
    8000,
    () => console.log("start Server")
);

このサーバに対してcurlで挙動を確認する。
curlのオプションとして、
-iオプションでレスポンスヘッダを表示し、HTTPのステータスコードを確認する。
--cacertで認証局をパラメータにつけることで-kオプション(サーバの証明書を検証しないオプション)なしで正しく通信できる。


$curl --key ./sample/client1-key.pem --cert ./sample/client1-crt.pem --cacert ./sample/ca-crt.pem https://localhost:8000/?o=First -i
HTTP/1.1 200 OK
Date: Mon, 30 Nov 2015 16:15:06 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Param:First
Org  :First
$ curl --key ./sample/client2-key.pem --cert ./sample/client2-crt.pem --cacert ./sample/ca-crt.pem https://localhost:8000/?o=Second -i
HTTP/1.1 200 OK
Date: Mon, 30 Nov 2015 16:14:02 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Param:Second
Org  :Second

getパラメータとクライアント証明書内の組織名が一致する場合は200 OKの応答。

$ curl --key ./sample/client1-key.pem --cert ./sample/client1-crt.pem --cacert ./sample/ca-crt.pem https://localhost:8000/?o=BadParam -i
HTTP/1.1 406 Not Acceptable
Date: Mon, 30 Nov 2015 16:15:38 GMT
Connection: keep-alive
Transfer-Encoding: chunked

Param:BadParam
Org  :First
$ curl --key ./sample/client1-key.pem  --cacert ./sample/ca-crt.pem https://localhost:8000/?o=First -i
curl: (35) NSS: client certificate not found (nickname not specified)

一致しない場合、クライアント証明書を送らない場合は、406 Not Acceptableやそもそも繋がらない挙動になります。

クライアントサイド

KeyStoreを作成

JDK付属のkeytoolを利用して、複数の証明書を一つのKeyStoreにまとめて保持します。
opensslで作ったPEMからpkcs12のKeyStoreに変換してkeystoreにまとめます。
keystoreは作成したJavaのプログラムで利用します。

$openssl pkcs12 -export -in sample/client1-crt.pem -inkey sample/client1-key.pem -out client1-store.pkcs12 -name client1 -noiter -nomaciter
$keytool -keystore keystore -importkeystore -srckeystore client1-store.pkcs12
$openssl pkcs12 -export -in sample/client2-crt.pem -inkey sample/client2-key.pem -out client2-store.pkcs12 -name client2 -noiter -nomaciter
$keytool -keystore keystore -importkeystore -srckeystore client2-store.pkcs12

※上記コマンドはパスワードの指定を要求される。同じパスワードを指定したほうがいいとどこかで見たが本当にダメかは未確認

作成したkeystoreの中身を確認、client1とclient2の名前を指定した証明書の登録があればOK。

$keytool -keystore keystore -list
キーストアのタイプ: JKS
キーストア・プロバイダ: SUN

キーストアには2エントリが含まれます

client2,2015/12/02, PrivateKeyEntry, 
証明書のフィンガプリント(SHA1): C6:81:F7:F3:47:73:74:65:8A:89:A0:03:0E:9F:91:AC:8F:72:C8:66
client1,2015/12/02, PrivateKeyEntry, 
証明書のフィンガプリント(SHA1): 2B:19:0A:04:E2:42:74:4A:97:CA:3F:95:53:4A:03:33:DB:7C:1F:81

ちなみに準備のときには間違えてtrustedCertEntryとして登録してどハマりしました。悲しい。
ちゃんとPrivateKeyEntryとしてKeyStoreに登録されていることを確認してください。

そして実装は長くなったので、Gistへ。
https://gist.github.com/seiketkm/0ff392c57dd50607c163
クライアント証明書の打ち分けのための重要なポイントは上級JSSE開発者のためのカスタムSSLのコードがほとんどそのままですが、AliasForcingKeyManagerというラッパークラスを作成することで、keystoreからaliasを元に選択可能にします。
ただ、なぜかkeystoreから取得したalias群は、"1.0.client2"、"2.0.client1"と意図しない数値が頭に付与されていたので、文字の比較はequalsではなくcontainsとしました。

gistのコードをjavacして、-DでKeyStore、KeyStoreのパスワード、オレオレ認証局を登録したTrustStore(作成方法略)、TrustStoreのパスワード を指定し実行します。

$java -Djavax.net.ssl.keyStore=./keystore -Djavax.net.ssl.keyStorePassword=hogehoge -Djavax.net.ssl.trustStore=./selfsigned.truststore -Djavax.net.ssl.trustStorePassword=hogehoge Client
=== HTTP GET Start === 
Param:First
Org  :First
=== HTTP GET End ===
=== HTTP GET Start ===
=== HTTP GET End ===
=== HTTP GET Start ===
=== HTTP GET End ===
=== HTTP GET Start ===
Param:Second
Org  :Second
=== HTTP GET End ===

実行結果はわかりづらいですが、Client.javaは証明書とパラメータが一致する200 OKのときだけ応答を出力するようにして、クライアント証明書とパラメータを変えながら通信させました。(ソース参照)
クライアント証明書1が使われて200 OKを返した通信とクライアント証明書2が使われて200 OKを返した通信が出力されており、証明書を変更しながら通信するという当初の目的を達成しました。

おわりに

長い記事におつきあいいただきありがとうございました。
クライアント証明書の動的変更のために、認証局とクライアント証明書の作成、node.jsでクライアント証明書やオレオレ認証局を扱う、ecma2015でのサーバ実装、javaのkeytool使うなど、手段が節操のない内容になってしまいました。
もうすぐクリスマスだし許してください。

さて明日のTIS Engineer Advent Calendarは?

明日は@0x9nです。明日の記事もお楽しみに!

参考

付録

この記事を書いて作った物群(github)

29
35
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
35