Apache によるSSLクライアント証明書の認証

経緯

Apache でクライアント証明書を用いた認証をどのように行うのか、挙動を確認してみました。

コード

まずはできたコードを。

/etc/httpd/conf.d/(仮想サイト).conf

仮想サイトの conf (SSLあり)。

<IfModule mod_ssl.c>
    <VirtualHost *:443>
        DocumentRoot "/var/www/hoge/web"
        ServerName www.sample.jp
        ServerAlias sample.jp
        RewriteEngine on
        RewriteCond %{HTTP_HOST} ^sample.jp$
        RewriteRule ^(.*)$       https://www.sample.jp/$1 [R=301,L]
        <Directory "/var/www/hoge/web">
            allow from all
            AllowOverride All
            Options FollowSymLinks
            Require all granted
            # SSL必須
            SSLRequireSSL
            # 違うパラメータ(SSL_CLIENT_S_DN_CNが異なる値、等)だと 403 になる
            SSLRequire ( %{SSL_CLIENT_S_DN_CN} eq "HogeHoge" )
            # このディレクティブを設定することで PHP の $_SERVER に SSLクライアント証明書の情報が渡されるようになる
            SSLOptions +StdEnvVars +ExportCertData
        </Directory>
        SSLEngine on
        SSLCertificateFile /etc/ssl/private/server.crt
        SSLCertificateKeyFile /etc/ssl/private/server.key
        SSLCACertificateFile /etc/ssl/private/client.crt
        # 証明書がインポートされていないと ERR_BAD_SSL_CLIENT_AUTH_CERT になる
        # optional にしてアクセス時にクライアント証明書を指定しないと $_SERVER に SSL_CLIENT_* がセットされない
        SSLVerifyClient require
        SSLVerifyDepth 1
    </VirtualHost>
</IfModule>

このような設定になりました。

クライアント証明書の発行

# echo "HogeHoge" > /etc/ssl/private/CA/.cacn
# echo "SOME_PASSPHRASE" > /etc/ssl/private/CA/.capass
# openssl genrsa -out  /etc/ssl/private/client.key 2048
# openssl req -new -key /etc/ssl/private/client.key -out /etc/ssl/private/client.csr -passout file:/etc/ssl/private/CA/.capass -subj "/C=JP/ST=/L=/O=/OU=/CN=HogeHoge"
# openssl x509 -req -in /etc/ssl/private/client.csr -signkey /etc/ssl/private/client.key -days 3650 -out /etc/ssl/private/client.crt
# cat /etc/ssl/private/client.key /etc/ssl/private/client.crt | openssl pkcs12 -password file:/etc/ssl/private/CA/.capass -export -out /etc/ssl/private/client.p12 -name "HogeHoge"

予めこのようなコマンドを叩いてクライアント証明書を発行してあります。対話式を経由することなくワンライナーで発行できるようにオプション盛り盛り。

index.php (Webアプリでのクライアント証明書の検証)

<?php
//var_dump($_SERVER);
// 予め CA の CN を読み込み
$CACN_FILEPATH = '/etc/ssl/private/CA/.cacn';
if(!file_exists($CACN_FILEPATH)) {
    header('HTTP/1.1 500 Internal Server Error.');
    echo 'コモンネームが読み込めませんでした。';
    exit();
}
$CA_CN = str_replace(["\r\n", "\r", "\n"], '', file_get_contents($CACN_FILEPATH));

// クライアント証明書
if(!isset($_SERVER['SSL_CLIENT_I_DN_CN']) || empty($_SERVER['SSL_CLIENT_I_DN_CN'])) {
    header('HTTP/1.1 403 Forbidden.');
    echo 'ブラウザにクライアント証明書がインポートされていません。';
    exit();
}
else if($_SERVER['SSL_CLIENT_I_DN_CN'] !== $CA_CN) {
    header('HTTP/1.1 403 Forbidden.');
    echo '誤ったクライアント証明書を使用しています。';
    exit();
}

// 処理

今回はサンプルなので適当ですが、念のため PHP でもクライアント証明書のパラメータを照合してチェックしています。

なお、 PHP 側からクライアント証明書の情報を参照するためには SSLOptions +ExportCertData ディレクティブが必要です。

また、念のため SSLVerifyClient requireSSLVerifyClient optional に変更すると、 PHP 側で引っかかることと、証明書をインポートしていないと $_SERVERSSL_CLIENT_* 系のパラメータがセットされないことを確認しました。

参考

Apache によるクライアント認証

ディレクティブ

openssl

PHP

$_SERVER に表出させる

パラメータの説明

(未使用) axios からは……?

Node.jsアプリ等ならばともかく、よくよく考えたらブラウザで動く JavaScript から証明書情報を取得する術がない (サーバ側にあったらそれを誰でも参照できる状態になってしまい意味がなくなるし、ローカルだとブラウザ上のJSからはアクセスが厳しい) ので考慮しない方向性。

そもそもWebアプリが動いているWebサーバにアクセスしている時点で認証をかけていればアプリにはアクセスできないので (API側は別途考える必要がありそうですが)。

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。