PHPを使ったサーバ証明書周り処理のメモ


職場で管理しているサーバ証明書の関連手続きを効率化するということで、久々のPHPで管理ツールを作りまして。
なかなか1ヶ所にまとめた情報がなかったので、その時のメモ。


1.秘密鍵文字列の取得

$opt = array(
    "private_key_bits"      => 2048,
    "private_key_type"      => OPENSSL_KEYTYPE_RSA
);
$pkey = openssl_pkey_new($opt);
openssl_pkey_export($pkey, $private_key);   // $private_keyに格納される

2.CSR文字列の取得
1.で取得した秘密鍵のリソース"$pkey"を使います。

$dn = array(
    "countryName"               => $country,
    "stateOrProvinceName"       => $state_of_province,
    "localityName"              => $locality,
    "organizationName"          => $organizational_name,
    "organizationalUnitName     => $organizational_unit,
    "commonName"                => $common_name,
    "emailAddress"              => ""
);
$csr = openssl_csr_new($dn, $pkey);
openssl_csr_export($csr, $csrout, true);    // $csroutに格納される

※上記項目のうち"organizationalUnitName"は入力必須ではありませんが、入力がない時は"organizationalUnitName"の要素自体を省略して関数を呼べばOKです。


3.秘密鍵文字列からCSR作成用のリソースIDを取得
秘密鍵とCSRを別々に作成する場合、いったん作成した秘密鍵文字列からCSR作成用のリソースID $pkeyを作成。

$pkey = openssl_pkey_get_private($priv_key);
if ($pkey !== false) {
    (上記2.の処理)
    ・・・・

4.サーバ証明書と中間証明書との整合性チェック

$hash1 = shell_exec("openssl x509 -issuer_hash -noout -in " . [サーバ証明書ファイルパス]);
$hash2 = shell_exec("openssl x509 -subject_hash -noout -in " . [中間証明書ファイルパス]);

で、hash1 = hash2であればOK。


5.サーバ証明書文字列のパース

$ssl = openssl_x509_parse([サーバ証明書データ]);

上記の$sslには連想配列が返りますが、具体的な内容についてはPHP公式のマニュアルにも明記されていません。
https://www.php.net/manual/ja/function.openssl-x509-parse.php

ちなみに、筆者の今の環境(PHP7.3.7/OpenSSL 1.0.2k)では下記の通りです。

[name] => 
[subject]           主体者情報
    [C] =>          国
    [ST] =>         都道府県
    [L] =>          市区町村
    [O} =>          組織名
    [OU] =>         部門名
    [CN] =>         サーバ名:ワイルドカード証明書の時は"*.hoge.com"などとなる
[hash]
[issuer]            発行者情報
    [C] =>          国
    [O] =>          組織名
    [CN] =>
[version] =>
[serialNumber] =>   シリアルNo
[serialNumberHex] =>
[validFrom] =>
[validTo] =>
[validFrom_time_t] => 有効期限開始日
[validTo_time_t] => 有効期限終了日
[signatureTypeSN] =>
[signatureTypeLN] =>
[signatureTypeNID] =>
[purpases]
    [1]
        [0] =>
        [1] =>
        [2] =>
    [2]
        [0] =>
        [1] =>
        [2] =>
    [3]
        [0] =>
        [1] =>
        [2] =>
    [4]
        [0] =>
        [1] =>
        [2] =>
    [5]
        [0] =>
        [1] =>
        [2] =>
    [6]
        [0] =>
        [1] =>
        [2] =>
    [7]
        [0] =>
        [1] =>
        [2] =>
    [8]
        [0] =>
        [1] =>
        [2] =>
    [9]
        [0] =>
        [1] =>
        [2] =>
[extensions]
    [authorityKeyIdentifier] =>
    [subjectKeyIdentifier] =>
    [subjectAltName] => オブジェクト代替名:"DNS:..."
    [keyUsage] =>
    [extendedKeyUsage] =>
    [csrDistributionPoints] =>
    [certificatePolicies] =>
    [authorityInfoAccess] =>
    [basicConstraints] =>
    [ct_precert_scts] =>

6.PKCS#12証明書内容のパース

$ssl_file_data = file_get_contents([PKCS12証明書ファイル名]);
if (openssl_pkcs12_read($ssl_file_data, $pkcs, $password) !== false) {
    $ssl = openssl_x509_parse($pkcs["cert"]);
  ・・・
  (以下、5.と同じ)

関数openssl_pkcs12_readで取得した連想配列$pkcsは、次の構造になっています。

[cert] => サーバ証明書文字列
[pkey] => 秘密鍵文字列
{extracerts]
    [0] => 中間証明書文字列?

7.PKCS#7証明書内容のパース

$ssl_file_data = file_get_contents([PKCS7証明書ファイル名]);
if (openssl_pkcs7_read($ssl_file_data, $pkcs) !== false) {
    $ssl = openssl_x509_parse($pkcs["0"]);
  ・・・
  (以下、5.と同じ)

上記の$sslには連想配列が返りますが、具体的な内容についてはPHP公式のマニュアルにも明記されていません。
https://www.php.net/manual/ja/function.openssl-pkcs7-read.php

ちなみに、筆者の今の環境(PHP7.3.7/OpenSSL 1.0.2k)では下記の通りです。

[0] :サーバ証明書情報
[1] :中間証明書情報
[2] :ルート証明書情報?(未確認)

8. 証明書チェーンの情報を外部サーバーから取得
指定したコモンネームのサーバー証明書~中間証明書の情報を外部サーバーから取得します。

$stm = stream_context_create(array(
    'ssl' => array('capture_peer_cert_chain' => true)    // ⇒証明書チェーンを指定
    ));
$rs1 = stream_socket_client(
    'ssl://' . $common_name . ':443',
    $err_no,
    $err_str,        // エラー時はここにエラー内容が返る
    30,
    STREAM_CLIENT_CONNECT,
    $stm
);

//***** サーバー証明書の情報取得 *****
$con = stream_context_get_params($rs1);
$inf = openssl_x509_parse($con["options"]["ssl"]["peer_certificate_chain"]["0"]);
・・・
//***** 中間証明書1の情報取得 *****
if (!empty($con["options"]["ssl"]["peer_certificate_chain"]["1"])) {
    $inf = openssl_x509_parse($con["options"]["ssl"]["peer_certificate_chain"]["1"]);
  ・・・・
}
//***** 中間証明書2の情報取得 *****
if (!empty($con["options"]["ssl"]["peer_certificate_chain"]["2"])) {
    $inf = openssl_x509_parse($con["options"]["ssl"]["peer_certificate_chain"]["1"]);
  ・・・・
}

9.(おまけ)証明書をファイルでダウンロード

header("Content-type: application/octet-stream")
header("Content-length: " . filesize([証明書ファイル名]);
header("Content-Disposition: attachment: filename=" . [ダウンロードファイル名]);

// ファイル出力
readfile([証明書ファイル名]);
unlink([証明書ファイル名]);
?>

最終行の"?>"の後に、改行含め余計な文字を加えると、ダウンロードファイルにゴミが混じるので要注意です。