自己署名証明書のサーバにJavaでHTTPS接続する

ニッチな内容ですが、だいぶハマったので。

## やりたいこと

新規に、「WEBサーバ」と「Javaクライアントアプリ」を作成した。
ここで、WEBサーバにJavaクライアントアプリからHTTPS接続をするテストをしたい。

### 越えなければならない壁

* 自己署名証明書を作成する
* Javaに証明書を信頼させる  
ここが厄介です、、ここではこの設定のための作業が多くなります。

### 想定読者
以下が分かっていることとします。特段説明をしません。

* SSL
* Spring Boot

### バージョン情報

* OS：Windows7
* WEBサーバ：Spring Boot 1.5.2.RELEASE
* Java：1.8.0_121
* 個人情報交換ファイルフォーマット：PKCS #12

## 作業手順

### 1. サーバ証明書の発行

1.1. openSSLのインストール

[ここ][]からインストーラをダウンロードして、インストールする。
対象サーバのbit数に合わせたものを使用したほうがいいらしい。通常/ライト版はどちらでも可。
[ここ]: http://slproweb.com/products/Win32OpenSSL.html

1.2. ディレクトリ/ファイル準備

簡易化のため、openSSLのデフォルト設定で証明書作成するので、固定のディレクトリ/ファイルを作成する。
以下を作成する。

1.2.1. ディレクトリ

* ca
* server
* client
* demoCA
* demoCA/newcerts

1.2.2. ファイル

* server/cacerts(Javaホーム/lib/security/cacertsをコピー)
* demoCA/index.txt（空ファイル0）
* demoCA/index.txt.attr（空ファイル）
* demoCA/serial（01と書いたテキストファイル）

1.3. 変数準備

以下、任意項目をまとめておく。
適宜自由に決めてもらってよいが、同じにしなければならない制約があったりするためここで明示しておく。（ちょっとしたハマりポイント）

変数名 : 説明　※明示のために''で囲っているが実際には記入不要。
'cakey.pem' : 認証局の秘密鍵ファイル名。
'cacert.pem' : 認証局の公開鍵ファイル名。
'capassword' : 認証局のパスワード。
'ST' : 地域名。__認証局とサーバで同じでないと署名できないので注意！__
'caname' : 認証局の名称。__認証局'caname'とサーバ'servername'で異なっていないと署名できないので注意!__
'sarvername' : WEBサーバの名称。__認証局'caname'とサーバ'servername'で異なっていないと署名できないので注意!__
'CN' : 証明書を作成するサーバのドメイン名。"localhost"やIPアドレスも可。
'.server_keystore' : WEBサーバのキーストア名。
'serverpassword' : WEBサーバのキーストアのパスワード
'server.crt' : WEBサーバ証明書
'server.x509' : WEBサーバ証明書（X509形式）
'server.p12' : 個人情報交換ファイル名

1.4. プライベート認証局の作成

認証局用の秘密鍵/公開鍵のキーペアを作成する。規格はX509（以後同様）

```sh
openssl req -keyout ca/'cakey.pem' -out ca/'cacert.pem' -new -x509
```

```sh
writing new private key to 'ca/cakey.pem'
Enter PEM pass phrase:'capassword'
Verifying - Enter PEM pass phrase:'capassword'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:'ST'
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:'caname'
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:'CN'
Email Address []:
```
作成した認証局の証明書をJavaの"信頼するルート認証局"としてインポートする。（パスワードは"changeit"）

```sh
keytool -import -file ca/'cacert.pem' -trustcacerts -alias 'caname' -keystore server/cacerts
```

1.5. サーバ証明書の作成

サーバのキーペアを作成する。（暗号鍵アルゴリズムはRSA）

```sh
keytool -genkey -keyalg RSA -alias 'servername' -keystore server/'.server_keystore'
```
```sh
キーストアのパスワードを入力してください:'serverpassword'
新規パスワードを再入力してください:'serverpassword'
姓名は何ですか。
  [Unknown]:  'CN'
組織単位名は何ですか。
  [Unknown]:
組織名は何ですか。
  [Unknown]:  'servername'
都市名または地域名は何ですか。
  [Unknown]:
都道府県名または州名は何ですか。
  [Unknown]:  'ST'
この単位に該当する2文字の国コードは何ですか。
  [Unknown]:
CN=CN, OU=Unknown, O=servername, L=Unknown, ST=ST, C=Unknownでよろしいですか。
  [いいえ]:  はい

<servername>の鍵パスワードを入力してください
        (キーストアのパスワードと同じ場合はRETURNを押してください):
```
証明書要求（CSR）を作成する。

```sh
keytool -certreq -alias servername -keystore server/.server_keystore -file server/server.csr
```
```sh
キーストアのパスワードを入力してください:'serverpassword'
```
（作成した）認証局を使って、証明書要求からサーバ証明書を署名する。

```sh
openssl ca -keyfile ca/'cakey.pem' -cert ca/'cacert.pem' -in server/server.csr -out server/'server.crt'
```
```sh
Using configuration from C:/OpenSSL-Win64/bin/openssl.cfg
Enter pass phrase for ca/cakey.pem:'capassword'                         
Check that the request matches the signature                                   
Signature ok                                      
Certificate Details:
        Serial Number: 1 (0x1)
        Validity                                                                  
            Not Before: May 17 02:20:49 2017 GMT                                  
            Not After : May 17 02:20:49 2018 GMT                                  
        Subject:                                                                  
            countryName               = Unknown                                   
            stateOrProvinceName       = ST                                     
            localityName              = Unknown                                   
            organizationName          = servername                                
            organizationalUnitName    = Unknown                                   
            commonName                = CN                                
        X509v3 extensions:                                                        
            X509v3 Basic Constraints:                                             
                CA:FALSE                                                          
            Netscape Comment:                                                     
                OpenSSL Generated Certificate                                     
            X509v3 Subject Key Identifier:                                        
                EE:1E:F6:4B:55:BF:59:4D:B9:A0:20:F7:CE:E9:C8:F0:B3:65:05:02       
            X509v3 Authority Key Identifier:                                      
                keyid:BC:57:86:E6:B0:E0:BE:CA:2D:47:6F:93:13:DE:0B:85:01:7B:DA:67 
                                                                                  
Certificate is to be certified until May 17 02:20:49 2018 GMT (365 days)          
Sign the certificate? [y/n]:y                                                     
                                                                                  
                                                                                  
1 out of 1 certificate requests certified, commit? [y/n]y                         
Write out database with 1 new entries                                             
Data Base Updated                                                                 
```
ここで、以下のエラーが末尾に出ていて、0バイトのサーバ証明書が出来ていたら失敗です。（ハマりポイント）

```sh
The stateOrProvinceName field needed to be the same in the
CA certificate (ST) and the request (ST)
```
認証局とサーバが同じ地域でなければならない制約なのですが、たまに__同じ地域__でも上記のようにエラーが発生します。
この場合は「-policy policy_anything」を付与して再実行します。

```sh
openssl ca -keyfile ca/'cakey.pem' -cert ca/'cacert.pem' -in server/server.csr -out server/'server.crt' -policy policy_anything
```
証明書をX509形式にする。

```sh
openssl x509 -in server/'server.crt' -out server/'server.x509'
```
キーストアにインポートする。
まず、認証局の証明書インポート

```sh
keytool -import -file ca/'cacert.pem' -alias 'caname' -keystore server/'.server_keystore'
```
```sh
キーストアのパスワードを入力してください:'serverpassword'
所有者: CN=CN, O=caname, ST=ST, C=Unknown
発行者: CN=CN, O=caname, ST=ST, C=Unknown
シリアル番号: 9310226b2ad5ebe7
有効期間の開始日: Fri Apr 28 20:17:20 JST 2017終了日: Sun May 28 20:17:20 JST 2017
証明書のフィンガプリント:
         MD5:  48:CA:F9:A9:85:88:44:5A:56:F3:9C:3E:16:D6:14:E1
         SHA1: 0F:F1:74:E3:C8:C9:0D:7D:3B:43:E0:7D:1A:10:D5:19:5B:72:8D:69
         SHA256: D6:C4:E0:78:BA:16:AF:A8:9C:23:51:18:31:E0:38:FC:57:2F:29:9C:BB:B8:3B:F4:83:85:D2:14:98:7D:2C:64
         署名アルゴリズム名: SHA256withRSA
         バージョン: 3

拡張:

#1: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
0000: BC 57 86 E6 B0 E0 BE CA   2D 47 6F 93 13 DE 0B 85  .W......-Go.....
0010: 01 7B DA 67                                        ...g
]
]

#2: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
  CA:true
  PathLen:2147483647
]

#3: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: BC 57 86 E6 B0 E0 BE CA   2D 47 6F 93 13 DE 0B 85  .W......-Go.....
0010: 01 7B DA 67                                        ...g
]
]

この証明書を信頼しますか。 [いいえ]:  はい
証明書がキーストアに追加されました
```
続いて、WEBサーバの証明書インポート

```sh
keytool -import -file server/'server.x509' -alias 'servername' -keystore server/'.server_keystore'
```
```sh
キーストアのパスワードを入力してください:'serverpassword'
証明書がキーストアに追加されました
```
キーストアからPKCS#12形式の証明書を作成する。

```sh
keytool -importkeystore -srckeystore server/'.server_keystore' -destkeystore server/'server.p12' -deststoretype PKCS12 -srcalias 'servername'
```
```sh
出力先キーストアのパスワードを入力してください:'serverpassword'
新規パスワードを再入力してください:'serverpassword'
ソース・キーストアのパスワードを入力してください:'serverpassword'
```

### 2. アプリケーションサーバに証明書を設定

2.1. 使用するもの

* 個人情報交換ファイル（'server.p12'）
* spring bootの設定ファイル（application.yml）

2.2. 修正内容

```application.yml
server:
  ssl:
    key-store: 'server.p12'
    key-store-password: 'serverpassword'
    keyStoreType: PKCS12
    keyAlias: servername
```

### 3. Javaクライアントアプリに証明書を信頼させる

3.1. 使用するもの

* キーストア（'.server_keystore'）
* 信頼するルート認証局情報（server/cacerts）

3.2. やること

JavaのVM引数に以下を設定する。

```sh
-Djavax.net.ssl.trustStore=（任意のディレクトリ）/server/cacerts -Djavax.net.ssl.keyStore=（任意のディレクトリ）/server/'server.p12' -Djavax.net.ssl.keyStorePassword='serverpassword'
```
これでJavaクライアントアプリからWEBサーバへHTTPS通信が可能となっているはずです。

## 終わりに

長かったですが、サーバ証明書を作る手順は一般的なものの通りです。
Javaの信頼するルート認証局情報に設定する方法がややイレギュラーで、開発時の限定的な作業となると思います。
ブラウザで行う分には数クリックで済む簡単な作業なんですけどね。。
可能であれば、開発時も自己署名証明書ではなく正規の証明書を作ってしまうのが楽だと思います。