防備録です。
PHPから openssl を叩いていますが、直接 openssl
を叩くときのコマンドもコメントに併記しています。
最終的にしたいこと
以下のような流れです。
- サーバが鍵ペアを発行する。
- クライアントは鍵ペアをブラウザに登録する。
- 以降はその公開鍵を用いてユーザを識別・認証する。
準備
Ubuntuの準備
ConoHa で Ubuntu20.04 をインストールし、ユーザを追加した状態からスタートします。この例では、ユーザ名は yukatayu
、ドメインが ssl-test.yukatayu.tech
で登録してある状態で操作しています。適宜読み替えてください。
とりあえず使いそうなものを入れます。特に nginx と openssl と letsencrypt が入っていれば良さそう。PHP は、とりあえず apt で楽に入る php7.4-fpm
を入れました。
sudo apt update
sudo apt upgrade -y
sudo apt install -y vim tree make git nginx openssl php7.4-fpm letsencrypt
CA鍵の生成
とりあえず作業フォルダを作ります。
sudo mkdir /var/client_cert
sudo chown yukatayu /var/client_cert
cd /var/client_cert
鍵ペアを作ります。楕円曲線の名前は 512
と見せかけて 521
なので注意してください。
openssl ecparam -genkey -name secp521r1 -out ca.key
鍵ペアにパスワードを付けます。今回は yukatayu
にしました。
openssl ec -in ca.key -out ca.key -aes256
鍵ペアを元に証明書を作ります。とりあえず有効期限は100年にします。
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 | (自分のメアド) |
読み取り権限をつけておきます。実際の運用ではセキュリティをもっといい感じにしておいてください。
chmod a+r ca.*
nginx のセットアップ
面倒なので、このセクションだけは root 権限で操作します。
sudo su -
cd /etc/nginx
サーバ証明書を発行します。とりあえずメアドを設定しないモードにしています。
設定したい場合は --non-interactive
と --register-unsafely-without-email
を外してください。
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 を雑に設定していきます。
vim sites-available/ssl-test.yukatayu.tech
ssl-test.yukatayu.tech (クリックして展開)
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;
}
最初の8行は、 http://
にアクセスが来たら https://
にリダイレクトする設定で、必須ではないです。
その後にある ssl_certificate
と ssl_certificate_key
はサーバ証明書、 ssl_client_certificate
と ssl_verify_client
はクライアント証明書の設定です。
その下の行で、 /private/
のみ認証が必要にしています。そして2つ目のポイントで、 fastcgi_param
が3つ並んでいる箇所は、クライアント認証の認証情報を PHP に受け渡しています。詳しくは 公式サイト を参照してください。
次に、SSL のいつもの設定をしていきます。
vim snippets/ssl.conf
ssl.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';
ssl_ciphers
は好みに合わせて変更してください。ラインナップが少し古めですが、現時点(2020年)では十分強いはずです。
設定ファイルを有効化します。
cd sites-enabled
rm default
ln -s /etc/nginx/sites-available/ssl-test.yukatayu.tech
設定ファイルをチェックします。
nginx -t
# > nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
# > nginx: configuration file /etc/nginx/nginx.conf test is successful
大丈夫そうなら nginx を再起動します。
systemctl restart nginx
PHP を書く
su
状態の人は戻ってください。別に戻らなくてもいいですけれど。
ここからはopensslを叩くだけです。とりあえず nginx で root
に指定した位置にフォルダを作ります。
mkdir -p /var/www
sudo chown yukatayu /var/www
cd /var/www
鍵発行部分の作成
vi keygen.php
コードを書きますが、PHPのコードをコマンドと勘違いすることは無いと思うので折りたたみ無しで書きます。流れとしては先ほどと同じですが、ルートCA証明書で署名する工程と、 pfx ファイルに固める工程が増えています。
$dn
と openssl_csr_sign
の第 4, 6 引数、及び $password
、 $friendlyName
は、発行する相手に合わせて変えてください。
あと、 $caPrevPw
は、先程CAキーに付けたパスワードです。この場合は yukatayu
です。
<?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 エリアの生成
mkdir private
cd private
vim index.php
とりあえず認証情報を雑に表示するだけです。クライアント証明書なしの場合は nginx で弾かれます。それと、コメント行は表示例です。
<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 を押すと、多分アクセスできるはずです。
おわりに
クライアント証明書が流行らない理由がわからないので、誰か教えてください。
それと今回のは説明用なのでなんか雑です。実戦投入したい場合はセキュリティの専門家にきちんと相談してください。