クライアント証明書を使った通信(curl+php)

# はじめに
クライアント証明書を使って、データ通信を行う開発をしなければいけなくなったので、そのメモです。
プログラムはcurl+phpです。

# 1.クライアント証明書を発行してもらうまでの準備作業
なにはともあれ、クライアント証明書を発行してもらわないと先にすすまないので、その前作業を記載します。
## 1-1.秘密鍵ファイルの作成

|項目|内容|指定する引数|
|:---|:---|:----------|
|鍵交換|RSA鍵長2048ビット|2048|
|暗号化|AES鍵長256ビット|-aes256|

```
$ openssl genrsa -aes256 -out mysecret.pem 2048
Generating RSA private key, 2048 bit long modulus
........................+++
...................................................................+++
e is 65537 (0x10001)
Enter pass phrase for mysecret.pem:パスワード ⇒ 入力
Verifying - Enter pass phrase for mysecret.pem:パスワード ⇒ 入力
```

## 1-2.秘密鍵ファイルをdecrypt
プログラムにパスワードを記載するわけにはいかないので、このファイルが必要です。

```
$ openssl rsa -in mysecret.pem -out mysecret-dec.pem
Enter pass phrase for mysecret.pem:パスワード ⇒ 入力
writing RSA key
```

## 1-3.CSR(証明書署名要求)の作成
```
$ openssl req -new -key mysecret-dec.pem -out mycsr.csr
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) [XX]:JP ⇒ 入力
State or Province Name (full name) []:AAA ⇒ 入力
Locality Name (eg, city) [Default City]:BBB ⇒ 入力
Organization Name (eg, company) [Default Company Ltd]:CCC ⇒ 入力
Organizational Unit Name (eg, section) []:DDD ⇒ 入力
Common Name (eg, your name or your server's hostname) []:EEE ⇒ 入力
Email Address []:fff@ggg.com ⇒ 入力

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:パスワード２ ⇒ 入力
An optional company name []:
```

このCSRファイルを相手先に送付して、クライアント証明書が送られてくるのを待ちます。
が、念のため、自分が作ったファイルの確認をします。

## 1-4. 秘密鍵ファイルの確認
```
$ openssl rsa -in mysecret-dec.pem -text
Private-Key: (2048 bit)
～省略～
writing RSA key
-----BEGIN RSA PRIVATE KEY-----
～省略～
-----END RSA PRIVATE KEY-----
```

## 1-5. CSRファイルの確認
```
$ openssl req -in mycsr.csr -text
Certificate Request:
    Data:
        Version: 0 (0x0)
        Subject: C=JP, ST=AAA, L=BBB, O=CCC, OU=DDD, CN=EEE/emailAddress=fff@ggg.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
～省略～
-----BEGIN CERTIFICATE REQUEST-----
～省略～
-----END CERTIFICATE REQUEST-----
```
**Subject**に自分が入力したのが表示されます。

# 2.クライアント証明書を発行されたので確認作業
相手先から、CA証明書とクライアント証明書が送られてきたので、その確認をします。

## 2-1.CA証明書の確認
```
$ openssl x509 -in cacert.pem -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            91:b0:b4:b5:6c:2f:9d:3f
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: 発行者情報
        Validity
            Not Before: 有効期限開始日
            Not After : 有効期限終了日
        Subject: 発行者情報
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
～省略～
-----BEGIN CERTIFICATE-----
～省略～
-----END CERTIFICATE-----
```

**Issuer**,**Subject**,**Not Before**,**Not After**に発行者の情報と有効期限が表示されます。

## 2-2.クライアント証明書の確認
```
$ openssl x509 -in clientcert.pem -text
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            91:b0:b4:b5:6c:2f:9d:80
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: 発行者情報
        Validity
            Not Before: Jan 23 11:22:33 2019 GMT ⇒ 有効期限開始日
            Not After : Jul 23 11:22:33 2019 GMT ⇒ 有効期限終了日
        Subject: C=JP, ST=AAA, L=BBB, O=CCC, OU=DDD, CN=EEE/emailAddress=fff@ggg.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
～省略～
-----BEGIN CERTIFICATE-----
～省略～
-----END CERTIFICATE-----
```

**Issuer**に発行者の情報
**Subject**に自分が入力した情報
**Not Before**,**Not After**に有効期限が表示されます。
ぐ、半年後に切れるのか。

# 3. curlで接続を試す。
まずは、curlコマンドで接続を試します。
これができなければ、プログラムをいくら作っても仕方がない。

## 3-1. "CA証明書確認あり"で試す
```
$ curl -v --cacert ./cacert.pem --cert ./clientcert.pem --key ./mysecret-dec.pem https://www.abc.com:12345/abc --output - --dump-header -
* About to connect() to www.abc.com port 12345 (#0)
*   Trying aaa.bbb.ccc.ddd...
* Connected to www.abc.com (aaa.bbb.ccc.ddd) port 12345 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: ./cacert.pem
  CApath: none
* NSS: client certificate from file
*       subject: CSR作成時に自分が入力した情報
*       start date:  クライアント証明書の有効期限開始日
*       expire date: クライアント証明書の有効期限終了日
*       common name: CSR作成時に自分が入力した情報
*       issuer: 発行者情報
* SSL connection using TLS_RSA_WITH_AES_256_CBC_SHA256
* Server certificate:
*       subject: 発行者情報
*       start date:   サーバ証明書の有効期限開始日
*       expire date:  サーバ証明書の有効期限終了日
*       common name: 発行者情報
*       issuer: 発行者情報
> GET / HTTP/1.1
～省略～

< HTTP/1.1 200 OK
～省略～
```

**cacert**に、CA証明書
**cert**に、クライアント証明書
**key**に、decryptした秘密鍵
を指定します。

# 4.curl+phpで試す。
## 4-1. プログラムの用意
```
<?php
$url     = "https://www.abc.com:12345/abc";
// CA証明書
$cacert  = "./cacert.pem";
// クライアント証明書
$sslcert = "./clientcert.pem";
// decryptした秘密鍵
$sslkey  = "./mysecret-dec.key";

// cURLを初期化する
$curl=curl_init();
// URLを指定する
curl_setopt($curl,CURLOPT_URL,$url);
// CA証明書の検証をする
curl_setopt($curl,CURLOPT_SSL_VERIFYPEER,true);
// CA証明書を指定
curl_setopt($curl,CURLOPT_CAINFO,$cacert);
// 1 は SSL ピア証明書に一般名が存在するかどうかを調べます。 
// 2 はそれに加え、その名前がホスト名と一致することを検証します。
curl_setopt($curl,CURLOPT_SSL_VERIFYHOST,2);
// クライアント証明書を指定
curl_setopt($curl,CURLOPT_SSLCERT,$sslcert);
// decryptした秘密鍵を指定
curl_setopt($curl,CURLOPT_SSLKEY ,$sslkey);
// curl_execの返り値を文字列にする
curl_setopt($curl,CURLOPT_RETURNTRANSFER,true);
// 通信内容を出力する
curl_setopt($curl,CURLOPT_VERBOSE,true);

// URLにアクセスし、結果を表示
$str=curl_exec($curl);

echo $str;

// cURLのリソースを解放する
curl_close($curl);
?>
```

## 4-2. 接続してみる。
```
$ php ssl_client.php
* About to connect() to www.abc.com port 12345 (#0)
*   Trying aaa.bbb.ccc.ddd...
* Connected to www.abc.com (aaa.bbb.ccc.ddd) port 12345 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
*   CAfile: ./cacert.pem
  CApath: none
* NSS: client certificate from file
*       subject: CSR作成時に入力した情報
*       start date:  クライアント証明書の有効期限開始日
*       expire date: クライアント証明書の有効期限終了日
*       common name: CSR作成時に入力した情報
*       issuer: 発行者情報
* SSL connection using TLS_RSA_WITH_AES_256_CBC_SHA256
* Server certificate:
*       subject: 発行者情報
*       start date:  サーバ証明書の有効期限開始日
*       expire date: サーバ証明書の有効期限終了日
*       common name: 発行者情報
*       issuer: 発行者情報
> GET /abc HTTP/1.1
～省略～
< HTTP/1.1 200 OK
```
無事できました。

# 5.いろいろ試す
せっかくなので、curlコマンドでいろいろ試した結果を記載します。

## 5-1. "CA証明書確認なし"で試す

```
$ curl -v -k --cert ./clientcert.pem --key ./mysecret-dec.pem https://www.abc.com:12345/abc --output - --dump-header -
* About to connect() to www.abc.com port 12345 (#0)
*   Trying aaa.bbb.ccc.ddd...
* Connected to www.abc.com (aaa.bbb.ccc.ddd) port 12345 (#0)
* Initializing NSS with certpath: sql:/etc/pki/nssdb
* skipping SSL peer certificate verification ⇒ ここだけ変わる
* NSS: client certificate from file
*       subject: CSR作成時に自分が入力した情報
*       start date:  クライアント証明書の有効期限開始日
*       expire date: クライアント証明書の有効期限終了日
*       common name: CSR作成時に自分が入力した情報
*       issuer: 発行者情報
* SSL connection using TLS_RSA_WITH_AES_256_CBC_SHA256
* Server certificate:
*       subject: 発行者情報
*       start date:   サーバ証明書の有効期限開始日
*       expire date:  サーバ証明書の有効期限終了日
*       common name: 発行者情報
*       issuer: 発行者情報
> GET / HTTP/1.1
～省略～

< HTTP/1.1 200 OK
～省略～
```

"CA証明書確認なし"でアクセスする場合、"-k"をオプションにつけます。
"-k"をつけないと、以下のように怒られます。

```
curl: (60) Peer's certificate issuer has been marked as not trusted by the user.
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.
```

# 5-2. CA証明書を違ったものに置き換えて接続を試す。
エラー内容として、以下が表示されます。

```
～省略～
* NSS error -8172 (SEC_ERROR_UNTRUSTED_ISSUER)
* Peer's certificate issuer has been marked as not trusted by the user.
* Closing connection 0
curl: (60) Peer's certificate issuer has been marked as not trusted by the user.
More details here: http://curl.haxx.se/docs/sslcerts.html
～省略～
```

# 5-3. 期限が切れたクライアント証明書で接続を試す。
エラー内容として、以下が表示されます。
"有効期限エラー"みたいなのは表示されないらしい。

```
～省略～
* NSS error -5938 (PR_END_OF_FILE_ERROR)
* Encountered end of file
* Closing connection 0
curl: (35) Encountered end of file
～省略～
```