Ubuntu+Nginx (+PHP) でクライアント証明書を発行するWebサイト

防備録です。
PHPから openssl を叩いていますが、直接 `openssl` を叩くときのコマンドもコメントに併記しています。

# 最終的にしたいこと

以下のような流れです。

* サーバが鍵ペアを発行する。
* クライアントは鍵ペアをブラウザに登録する。
* 以降はその公開鍵を用いてユーザを識別・認証する。

# 準備

## Ubuntuの準備

　ConoHa で Ubuntu20.04 をインストールし、ユーザを追加した状態からスタートします。この例では、ユーザ名は `yukatayu` 、ドメインが `ssl-test.yukatayu.tech` で登録してある状態で操作しています。適宜読み替えてください。

　とりあえず使いそうなものを入れます。特に nginx と openssl と letsencrypt が入っていれば良さそう。PHP は、とりあえず apt で楽に入る `php7.4-fpm` を入れました。

```bash
sudo apt update
sudo apt upgrade -y
sudo apt install -y vim tree make git nginx openssl php7.4-fpm letsencrypt
```

## CA鍵の生成

　とりあえず作業フォルダを作ります。

```bash
sudo mkdir /var/client_cert
sudo chown yukatayu /var/client_cert
cd /var/client_cert
```

　鍵ペアを作ります。楕円曲線の名前は `512` と見せかけて `521` なので注意してください。

```bash
openssl ecparam -genkey -name secp521r1 -out ca.key
```

　鍵ペアにパスワードを付けます。今回は `yukatayu` にしました。

```bash
openssl ec -in ca.key -out ca.key -aes256
```

　鍵ペアを元に証明書を作ります。とりあえず有効期限は100年にします。

```bash
openssl req -new -x509 -days 36500 -key ca.key -out ca.crt
```

　パラメータは以下のようにしました

| 項目 | 自分が設定した値 |
| --- | --- |
| Country Name | JP |
| State or Province Name | Tokyo |
| Locality Name | Meguro |
| Organization Name | Yukatayu Project |
| Organizational Unit Name | Engineering Department |
| Common Name | ssl-test.yukatayu.tech |
| Email Address | (自分のメアド) |

　読み取り権限をつけておきます。実際の運用ではセキュリティをもっといい感じにしておいてください。

```bash
chmod a+r ca.*
```

## nginx のセットアップ

　**面倒なので、このセクションだけは root 権限で操作します。**

```bash
sudo su -
cd /etc/nginx
```

　サーバ証明書を発行します。とりあえずメアドを設定しないモードにしています。
設定したい場合は `--non-interactive` と `--register-unsafely-without-email` を外してください。

```bash
certbot certonly \
    --standalone \
    --non-interactive \
    --agree-tos \
    --register-unsafely-without-email \
    --domains ssl-test.yukatayu.tech \
    --pre-hook 'systemctl stop nginx' \
    --post-hook 'systemctl start nginx'
```

ここで

> IMPORTANT NOTES:
> - Congratulations! Your certificate and chain have been saved at ...

みたいなことを言われたら成功しています。

次に nginx を雑に設定していきます。

```bash
vim sites-available/ssl-test.yukatayu.tech
```

<details><summary><b>ssl-test.yukatayu.tech (クリックして展開)</b></summary><div>

```conf
server {
    listen 80; 
    listen [::]:80;
    server_name ssl-test.yukatayu.tech;
    location / { 
        return 302 https://$host$request_uri;   #301でも良さそう
    }   
}

server {
    server_name ssl-test.yukatayu.tech;
    root /var/www;

    ssl_certificate /etc/letsencrypt/live/ssl-test.yukatayu.tech/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ssl-test.yukatayu.tech/privkey.pem;

    ssl_client_certificate "/var/client_cert/ca.crt";
    ssl_verify_client optional;  # onだと常時

    location / { 
        # 認証不要
    }   

    location /private/ {
        # 認証が必要
        if ($ssl_client_verify != SUCCESS) {
            return 403;
        }
    }   

    location ~ \.php$ {
        #fastcgi_pass   127.0.0.1:9000;
        fastcgi_pass   unix:/run/php/php7.4-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include    fastcgi_params;
        fastcgi_param SSL_CLIENT_I_DN $ssl_client_i_dn;
        fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;
        fastcgi_param SSL_CLIENT_F_PR $ssl_client_fingerprint;
    }   

    include /etc/nginx/snippets/ssl.conf;
}

```

</div></details>

　最初の8行は、 `http://` にアクセスが来たら `https://` にリダイレクトする設定で、必須ではないです。
　その後にある `ssl_certificate` と `ssl_certificate_key`はサーバ証明書、 `ssl_client_certificate` と `ssl_verify_client` はクライアント証明書の設定です。
　その下の行で、 `/private/` のみ認証が必要にしています。そして2つ目のポイントで、 `fastcgi_param` が3つ並んでいる箇所は、クライアント認証の認証情報を PHP に受け渡しています。詳しくは [公式サイト](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#variables) を参照してください。

次に、SSL のいつもの設定をしていきます。

```conf
vim snippets/ssl.conf
```

<details><summary><b>ssl.conf (クリックして展開)</b></summary><div>

```conf
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on; 

    ssl_session_cache shared:le_nginx_SSL:1m;
    ssl_session_timeout 1d; 

    ssl_session_tickets off;

    gzip on; 
    gzip_types
        text/plain
        text/xml
        text/css
        application/xml
        application/xhtml+xml
        application/rss+xml
        application/atom_xml
        application/javascript
        application/x-javascript
        application/x-httpd-php;
    gzip_disable    "MSIE [1-6]\.";
    gzip_disable    "Mozilla/4";
    gzip_comp_level 1;
    gzip_proxied    any;  
    gzip_vary   on; 
    gzip_buffers    4 8k; 
    gzip_min_length 1100;
    index  index.html index.htm index.php;

    ## Static Resources
    location ~* \.(css|js|jpeg|jpg|gif|png|ico)$ {
        expires 3d;
        break;
    }

    location ~ /\.ht { deny  all; }
    location = /robots.txt { access_log off; log_not_found off; }
    location = /favicon.ico { access_log off; log_not_found off; }


    # OCSP Stapling --- 
    # fetch OCSP records from URL in ssl_certificate and cache them
    ssl_stapling on; 
    ssl_stapling_verify on; 

    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS!DH';
```

</div></details>

　`ssl_ciphers` は好みに合わせて変更してください。ラインナップが少し古めですが、現時点(2020年)では十分強いはずです。

　設定ファイルを有効化します。

```bash
cd sites-enabled
rm default
ln -s /etc/nginx/sites-available/ssl-test.yukatayu.tech
```

　設定ファイルをチェックします。

```bash
nginx -t
# > nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# > nginx: configuration file /etc/nginx/nginx.conf test is successful
```

大丈夫そうなら nginx を再起動します。

```bash
systemctl restart nginx
```

# PHP を書く

　`su` 状態の人は戻ってください。別に戻らなくてもいいですけれど。
　ここからはopensslを叩くだけです。とりあえず nginx で `root` に指定した位置にフォルダを作ります。

```bash
mkdir -p /var/www
sudo chown yukatayu /var/www
cd /var/www
```


## 鍵発行部分の作成

```bash
vi keygen.php
```

　コードを書きますが、PHPのコードをコマンドと勘違いすることは無いと思うので折りたたみ無しで書きます。流れとしては先ほどと同じですが、ルートCA証明書で署名する工程と、 pfx ファイルに固める工程が増えています。

　`$dn` と `openssl_csr_sign` の第 4, 6 引数、及び `$password`、 `$friendlyName` は、発行する相手に合わせて変えてください。

　あと、 `$caPrevPw` は、先程CAキーに付けたパスワードです。この場合は `yukatayu` です。

```php
<?php
// クライアント証明書用の鍵
// openssl ecparam -genkey -name secp521r1 -out ca.key
// RSA が好きなら openssl genrsa -des3 -out user.key 4096
$privateKey =
	openssl_pkey_new([
		'private_key_bits' => 512,  // RSAの 15360 bit くらいの強さらしい
		'private_key_type' => OPENSSL_KEYTYPE_EC,
		'curve_name' => 'secp521r1',
		// RSA が好きなら
		// 'private_key_bits' => 4096,
		// 'private_key_type' => OPENSSL_KEYTYPE_RSA,
	]);

// 署名
// openssl req -new -key user.key -out user.csr
$dn = [
	'countryName' => 'JP',
	'stateOrProvinceName' => 'Tokyo',
	'localityName' => 'Meguro',
	'organizationName' => 'Yukatayu',
	'organizationalUnitName' => 'Some Team',
	'commonName' => 'yukatayu.tech',
	'emailAddress' => 'yukatayu@example.com',
];

$csr =
	openssl_csr_new(
		$dn,
		$privateKey,
		[
			'digest_alg' => 'sha256',
		]);

// CSRの署名
// openssl x509 -req -days 365 -in user.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out user.crt
$caCert = '/var/client_cert/ca.crt';
$caPrev = '/var/client_cert/ca.key';
$caPrevPw = 'yukatayu';

$x509 =
	openssl_csr_sign(
		$csr,
		"file://{$caCert}",
		["file://{$caPrev}", $caPrevPw],
		3650,  // days
		['digest_alg' => 'sha256'],
		1  // serial
	);

// PKCS #12 (PFX)の作成
// openssl pkcs12 -export -out user.pfx -inkey user.key -in user.crt -certfile ca.crt
$password = 'nyan';
$friendlyName = 'Yukatayu Secret Key';

$pkcs12 = null;
openssl_pkcs12_export(
	$x509,
	$pkcs12,
	$privateKey,
	$password,
	[
		// 'extracerts' => $CAcert,
		'friendly_name' => $friendlyName,
	]);

// 出力
header('Content-Type: application/x-pkcs12');
header('X-Content-Type-Options: nosniff');
header('Content-Length: ' .strlen($pkcs12));
header('Content-Disposition: attachment; filename="yukatayu_tech_client.pfx"');
header('Connection: close');
print($pkcs12);
exit();
```

　`curve_name` は `print_r(openssl_get_curve_names());` などをすることで分かります。また、対応する openssl のコマンドをコメントで記述しておきましたので、これらをして手で生成することができます。

## private エリアの生成

```bash
mkdir private
cd private
vim index.php
```

　とりあえず認証情報を雑に表示するだけです。クライアント証明書なしの場合は nginx で弾かれます。それと、コメント行は表示例です。

```php
<pre>
Welcome:

SSL_CLIENT_I_DN: <?= $_SERVER['SSL_CLIENT_I_DN'] ?>
<!-- emailAddress=yukatayu.dev@gmail.com,CN=ssl-test.yukatayu.tech,OU=Engineering Department,O=Yukatayu Project,L=Meguro,ST=Tokyo,C=JP -->

SSL_CLIENT_S_DN: <?= $_SERVER['SSL_CLIENT_S_DN'] ?>
<!-- emailAddress=yukatayu@example.com,CN=yukatayu.tech,OU=Some Team,O=Yukatayu,L=Meguro,ST=Tokyo,C=JP -->

SSL_CLIENT_F_PR: <?= $_SERVER['SSL_CLIENT_F_PR'] ?>
<!-- b6b84c9573998e096d1bea593c79b0dbae862145 -->
```

# 動作確認

## アクセス制限の確認
　とりあえず `ssl-test.yukatayu.tech/private/` にアクセスしてみると 403 が返ります。

## 鍵ペアを発行する
　ブラウザから `ssl-test.yukatayu.tech/keygen.php` にアクセスすると pfx ファイルが降ってきます。

## 鍵ペアをブラウザに登録する
　私はとりあえずFirefox派なので、その手順を説明します。

　まずブラウザの設定を開きます。「プライバシとセキュリティ」から「証明書を表示…(C)」を押します。「あなたの証明書」から「インポート(M)…」を押し、先程の pfx ファイルを開きます。
　上記のコードでは `$password = 'nyan';` となっているので、パスワードの `nyan` を入力するとインポートできます。

## アクセスしてみる
　`ssl-test.yukatayu.tech/private/` にアクセスしてみると、「個人証明書の要求」というプロンプトが出るはずです。 出ない場合は **Shiftを押しながら** リロードしてみてください。
　先程インポートした証明書を選択して OK を押すと、多分アクセスできるはずです。

# おわりに
　クライアント証明書が流行らない理由がわからないので、誰か教えてください。
　それと今回のは説明用なのでなんか雑です。実戦投入したい場合はセキュリティの専門家にきちんと相談してください。