PHPのopenssl関数で動的に自己署名クライアント証明書を発行する


はじめに

今回はPHPのopenssl関数を使ってクライアント証明書を出力する方法をご紹介します。
利用の際は、セキュリティ面についてご自身でも検証、理解の上でご利用ください。

検証バージョン

以下の環境で動作確認しました。
OS: CentOS 7.8
PHP: 5.5.38
openssl: 1.0.2k
Apache: 2.4.6
※Windows 10上にVagrantで構築しました

前提

事前にApache上にプライベート認証局、クライアント証明書をセットアップしておきます。
以下の記事が非常に参考になりました。
https://qiita.com/mitzi2funk/items/602d9c5377f52cb60e54
上記が完了している前提で以下をご覧ください。

PHPのopenssl関数について

opensslコマンドをPHPから利用できるようにラップした関数群です。
openssl関数の中でも今回は以下を利用します。
以下がそのまま処理の流れになっています。

openssl_pkey_new(クライアント証明書用のキーペア作成)

openssl_csr_new(クライアント証明書用のCRS作成)

openssl_csr_sign(クライアント証明書への署名)

openssl_pkcs12_export(クライアント証明書をPKCS12形式へ変換)

コード

ApacheやCA認証局の設定関連が成功していれば、以下のプログラムで、Windows OSなどで利用できるクライアント証明書形式(pkcs12 / *.pfx)を取得できます。

cert.php
<?php

// 各種条件
$client_name = "taro_yamada"; // 認証対象の名前など
$secure_domain = "secure.example.com"; // サーバ名など
$config_file = "/etc/pki/tls/openssl-client.cnf"; // クライアント証明書発行用のconfig
$password = "xxxxxxxx"; // クライアント証明書用パスワード(証明書インストール時に使用)
$cacert_path = "/etc/pki/CA/cacert.pem"; // CA証明書のパス
$cakey_path = "/etc/pki/CA/private/cakey.pem"; // CA秘密鍵のパス
$ca_password = "xxxxxxxx"; // CA証明書作成時に設定したパスワード

// CSR作成
$dn = array(
    "countryName" => "JP",
    "stateOrProvinceName" => "Tokyo",
    "localityName" => "Shibuya-ku",
    "organizationName" => "My Company Co., Ltd.",
    "organizationalUnitName" => "System Dept.",
    "commonName" => $client_name,
    "emailAddress" => "[email protected]"
);

// 新しい 秘密鍵(と公開鍵の) キーペアを生成します
$privkey = openssl_pkey_new(array(
    "private_key_bits" => 2048,
    "private_key_type" => OPENSSL_KEYTYPE_RSA,
));

// CSR を生成します
$configargs = array(
    'digest_alg' => 'sha256',
    'config' => $config_file,
);
$csr = openssl_csr_new($dn, $privkey, $configargs);

// 自己署名の証明書を生成します。365日有効です
$cacert_string = file_get_contents($cacert_path);
$cakey_string = file_get_contents($cakey_path);
$privkey_ca = array($cakey_string, $ca_password);
$x509 = openssl_csr_sign($csr, $cacert_string, $privkey_ca, $days=365, $configargs);

// 秘密鍵、CSR と自己署名証明書をあとで使うために保存します。
//openssl_csr_export($csr, $csrout) and var_dump($csrout);
//openssl_x509_export($x509, $certout) and var_dump($certout);
//openssl_pkey_export($privkey, $pkeyout, $password) and var_dump($pkeyout);
// ↑必要に応じてDBなどに保存

// PKCS12形式へエクスポート($cerificate_outに結果が代入される)
$friendly_name = $client_name . "." . $secure_domain;
$args = array(
               //'extracerts' => $CAcert,
               'friendly_name' => $friendly_name,
              );
openssl_pkcs12_export($x509, $cerificate_out, $privkey, $password, $args);

// 起きたエラーを表示します。
$e = openssl_error_string();
if ($e) {
    while ($e !== false) {
        echo $e . "\n";
    }
    exit;
}

// pfxファイルをダウンロード(WEBサーバ上で実行してダウンロードする場合)
//header("Content-Type: application/force-download");
//header("Content-Length: ".strlen($cerificate_out));
//header("Content-Disposition: attachment; filename=\"{$friendly_name}.pfx\"");
//echo $cerificate_out . "\n";

// ファイル出力
file_put_contents("/path/to/{$friendly_name}.pfx", $cerificate_out);

※説明上簡易的な実装にしています
※WEBアプリケーションで上記を管理するのはセキュリティ的に問題がある可能性があるので、あくまで参考までとしていただければと思います(証明書関連ファイルのパーミッションなど)

メモ1

以下については、ドキュメントなどではopenssl_csr_signの第2引数(証明書の指定)を「file://etc/pki/CA/cacert.pem」の形式で直接ファイル名を指定できる様子でしたが、私の環境ではうまく動作しなかったので事前にテキストを読み込んで、そのテキストを与えるようにしました。

$cacert_string = file_get_contents($cacert_path);
$cakey_string = file_get_contents($cakey_path);
$privkey_ca = array($cakey_string, $ca_password);
$x509 = openssl_csr_sign($csr, $cacert_string, $privkey_ca, $days=365, $configargs);

openssl_csr_signの第2引数については、以下のCの実装などを見ると詳細がわかると思います。
https://github.com/php/php-src/blob/bcd100d812b525c982cf75d6c6dabe839f61634a/ext/openssl/openssl.c#L2491
https://github.com/php/php-src/blob/bcd100d812b525c982cf75d6c6dabe839f61634a/ext/openssl/openssl.c#L1240

メモ2

openssl_x509_exportで取得できる証明書データをDBなどに保存しておくことで、クライアント証明書を個別に無効化できるはずです。
(クライアント証明書からrevokeリストを作成→ApacheにSSLCARevocationFileとしてrevokeリストを読み込ませる)