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

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

***
#### 1. 秘密鍵文字列の取得

```PHP:
$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"を使います。

```PHP:
$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に格納される
```
:::note info
※上記項目のうち"organizationalUnitName"は入力必須ではありませんが、入力がない時は"organizationalUnitName"の要素自体を省略して関数を呼べばOKです。
:::
***
#### 3. 秘密鍵文字列からCSR作成用のリソースIDを取得
秘密鍵とCSRを別々に作成する場合、いったん作成した秘密鍵文字列からCSR作成用のリソースID $pkeyを作成。

```PHP:
$pkey = openssl_pkey_get_private($priv_key);
if ($pkey !== false) {
    （上記2.の処理）
    ・・・・
```
***
#### 4.サーバ証明書と中間証明書との整合性チェック

```PHP:
$hash1 = shell_exec("openssl x509 -issuer_hash -noout -in " . [サーバ証明書ファイルパス]);
$hash2 = shell_exec("openssl x509 -subject_hash -noout -in " . [中間証明書ファイルパス]);
```
で、hash1 = hash2であればOK。
***
#### 5.サーバ証明書文字列のパース

```PHP:
$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証明書内容のパース

```PHP:
$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証明書内容のパース

```PHP:
$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. 証明書チェーンの情報を外部サーバーから取得
指定したコモンネームのサーバー証明書～中間証明書の情報を外部サーバーから取得します。

```PHP:
$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. ルート証明書、サーバ証明書、中間証明書の証明書チェーンのチェック

```PHP:
$tmpfile = "tmp/" . uniqid("hogehoge", false);                              ・・・(1)
shell_exec("cat " . （ルート証明書ファイル名） . " | grep '' > " . $tmpfile);  ・・・(2)
shell_exec("cat " . （中間証明書ファイル名） . " >> " . $tmpfile);
$ret = shell_exec("openssl verify -CAfile " . $tmpfile . " " . （サーバ証明書ファイル名）);
if (mb_substr(trim($ret), - 2) != "OK") {
    echo "入力のルート証明書は、サーバ証明書、中間証明書とのチェーンが確認できません";
}
unlink($tmpfile);
```
1) OpenSSLを直接たたくとワンライナーでできるのですが、shell_exec関数経由ではうまく動かないので、一時ファイルを用いてステップ分割します。
2) ルート証明書ファイルの改行コードがCR+LF、かつファイルの最後に改行がないとき、証明書内容の直後に後続ファイルのデータがマージされることがあるので、grepで強制的に改行を補完しています。

（参考）
[hacknote OpenSSLコマンドでSSL証明書の証明書チェーン検証](https://hacknote.jp/archives/29005/)

***
#### 10.（おまけ）証明書をファイルでダウンロード

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

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

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