PHP cURLの証明書エラー:VERIFYPEERを無効にする危険性とCAバンドルの設定方法
作成日:2026.11.02
PHPのcURLで「unable to get local issuer certificate」が出たとき、CURLOPT_SSL_VERIFYPEERをfalseにする対処が危険な理由を解説します。証明書の信頼性検証とホスト名照合の違いを整理し、WindowsでのCAバンドルの取得、curl.cainfoの設定、反映確認までを紹介します。
目次
PHPのcURLでHTTPSのAPIへ接続すると、次のようなエラーが出ることがあります。
SSL certificate problem: unable to get local issuer certificate
このエラーへの対処として、CURLOPT_SSL_VERIFYPEERをfalseにする説明を見かけることがあります。ただ、これは証明書を信頼できるかどうかの検証を無効にする設定です。エラーが消えても、そのまま使うのは危険です。
今回は、何が危険なのかを整理してから、WindowsのPHPでCA証明書や設定を確認する流れを紹介します。PHPのcURLによる通信処理の基本は、外部API連携の実装記事も参照してください。
エラーを消すことと、安全に接続することは別問題
HTTPSでは、通信を暗号化するだけでなく、接続相手が意図したサーバーなのかも確認します。そこで使うのがサーバー証明書です。
証明書には接続先の名前などが書かれていますが、その名前が書いてあるだけで信用してよいわけではありません。信頼する認証局(CA)まで署名をたどって検証できるかを確認します。サーバー証明書とルートCAの間に、中間CAの証明書が必要になる場合もあります。
unable to get local issuer certificateは、その検証に必要な発行者の証明書を見つけられず、信頼チェーンを構築できない場合に出るエラーです。クライアントが参照するCA証明書や、サーバーから送られる中間証明書などを調べる必要があります。
ここでCURLOPT_SSL_VERIFYPEERをfalseにすると、証明書の信頼性の検証を省略します。信頼できることを確認して接続できたのではなく、確認せずに接続したという状態になります。
ホスト名の確認を残しても、それだけでは足りない
PHPのcURLには、名前の似た二つの設定があります。
| 設定 | 確認すること | 指定する値 |
|---|---|---|
CURLOPT_SSL_VERIFYPEER | 証明書を信頼するCAまで検証できるか | true |
CURLOPT_SSL_VERIFYHOST | 証明書の名前がURLのホスト名・IPアドレスに対応しているか | 2 |
両方とも既定では検証が有効ですが、コードに明示する場合もこの組み合わせにします。
二つの検証は独立しています。VERIFYHOSTを有効にしていても、VERIFYPEERを無効にすると、証明書の名前は確認しても、その証明書を信頼できるかどうかは確認しません。
例えば、攻撃者が接続先と同じ名前を入れた自己署名証明書を作ったとします。その証明書でも名前の条件は満たせます。通信経路へ介入されるなどの条件がそろうと、証明書の信頼性を検証しないクライアントは、偽の接続先を受け入れてしまうおそれがあります。
その結果、APIキーや送信データを攻撃者へ渡したり、偽のレスポンスを受け取ったりする可能性があります。通信が暗号化されていても、その相手が攻撃者では困りますよね。
libcurl公式のVERIFYPEERの説明でも、検証の無効化による中間者攻撃の危険性と、暗号化だけでは接続相手を確認できないことが説明されています。
名前が一致する証明書で、検証の違いを確認する
検証用CAから、localhostの名前を持つサーバー証明書を発行し、ローカルのHTTPSサーバーへ接続して比較しました。サーバーは同じ証明書を使い、クライアント側の設定だけを変えています。VERIFYHOSTはすべて2です。
確認した環境は、Windows上のPHP 8.3.15 CLI、libcurl 8.7.0-DEV、TLSバックエンドのOpenSSL 3.0.15です。証明書の生成にはOpenSSL 3.4.0、ローカルサーバーにはPythonを使いました。実サービスや実際の認証情報を使った比較ではありません。
| CAファイル | VERIFYPEER | 結果 |
|---|---|---|
| 発行者を含まない、別の検証用CA | true | エラー60:unable to get local issuer certificate |
| 同じ別の検証用CA | false | HTTP 200で本文を取得 |
| 実際に発行した検証用CA | true | HTTP 200で本文を取得 |
2行目は、信頼性を確認できない証明書でも、検証を省けば通信できてしまう例です。対処として目指すのは3行目のように、信頼するCAを適切に指定し、検証を有効にしたまま接続できる状態です。
なお、ここでは説明用に作ったCAを指定しています。実際の接続でエラーになったからといって、相手から受け取った証明書を無条件に信頼対象へ追加してよいわけではありません。
WindowsのPHPで、使っている設定を確認する
エラー文だけでは、CAファイルがないのか、参照先が違うのか、サーバー側の証明書が不足しているのかまでは分かりません。まず、実際に動いているPHPを確認します。以下はWindowsのPowerShellでの例です。
Get-Command php -CommandType Application | Select-Object -ExpandProperty Source
php -v
php --ini
続いて、次をtls-env.phpとして保存します。cURL拡張が有効なPHPで実行してください。
<?php
$version = curl_version();
printf("PHP: %s\n", PHP_VERSION);
printf("SAPI: %s\n", PHP_SAPI);
printf("php.ini: %s\n", php_ini_loaded_file() ?: '(none)');
printf("libcurl: %s\n", $version['version']);
printf("TLS: %s\n", $version['ssl_version']);
printf("curl.cainfo: %s\n", ini_get('curl.cainfo') ?: '(not set)');
php .\tls-env.php
TLSバックエンドがOpenSSLで、curl.cainfo: (not set)と表示された場合は、下の手順でCAバンドルを用意し、php.iniにそのパスを設定します。既にパスが表示されている場合は、指定先にファイルが存在するか、PHPから読み取れるかを確認してください。
また、curl公式の証明書検証の説明では、Schannelを使う構成はWindowsの証明書ストアを利用するとされています。OpenSSLを使う構成と、同じ前提で設定を決めないようにします。curl.exeで成功しても、PHPのcURLが同じTLSバックエンドやCAを使っているとは限りません。
Web側でエラーが出る場合は、Web側のPHPでも版・SAPI・設定ファイル・CA参照先を確認します。CLIで見たphp.iniを直しても、別の設定を使っていれば反映されません。診断用の情報は公開せず、管理下の環境で確認して、不要になった診断用ファイルは削除してください。
curl.cainfoが未設定の場合の対処手順
ここでは、OpenSSLを使うPHPで公開CAの証明書を検証するために、CAバンドルを取得して設定します。例ではC:\php-certs\cacert.pemに配置しますが、別の場所でも構いません。
1. CAバンドルをダウンロードして配置する
ブラウザでcurl公式のCA Extractを開き、ページ内のcacert.pemのリンクからファイルをダウンロードします。MozillaのCAストアをPEM形式にした、公開CAの証明書をまとめたファイルです。
エクスプローラーでC:\php-certsフォルダーを作成し、ダウンロードしたファイルをcacert.pemという名前で配置します。拡張子が重複してcacert.pem.txtなどになっていないかも確認してください。配置先はPHPを動かすユーザーが読み取れる場所にします。
取得したファイルのSHA-256は、CA Extractのページにある値と照合します。取得時もHTTPSの検証を無効にせず、検証が正常に働くブラウザを使ってください。
2. 使用中のphp.iniへパスを設定する
先ほど実行したphp --iniのLoaded Configuration File、またはtls-env.phpのphp.ini:に表示されたファイルをエディターで開きます。PHPのインストール先を推測して、別のphp.iniを編集しないようにします。
ファイル内でcurl.cainfoを検索します。;curl.cainfo =のような行があれば、先頭の;を外し、配置したファイルの絶対パスを設定します。行がなければ追加してください。既に有効な設定行がある場合は、その行を修正して重複させないようにします。
curl.cainfo = "C:/php-certs/cacert.pem"
PHP公式のcURL設定の説明では、curl.cainfoには絶対パスを指定する必要があります。ファイルが存在することと、PHPを動かすユーザーが読み取れることも確認します。
3. 設定の反映を確認して、元の通信処理を試す
php.iniを保存したら、もう一度php .\tls-env.phpを実行します。CLIは起動ごとに設定を読み込むので、次のように配置先のパスが表示されれば、設定が読み込まれています。
curl.cainfo: C:/php-certs/cacert.pem
(not set)のままなら、編集したファイルが実際に読み込まれているか、行頭の;が残っていないかを確認します。パスが表示されても証明書の内容まで確認できたわけではないので、続けて元の通信処理を実行してください。VERIFYPEERはtrue、VERIFYHOSTは2にしておきます。
Apacheモジュールなど、PHPを常駐させる構成では、利用するプロセスを再起動・再読み込みしてからWeb側でも確認します。CLIで設定が変わっただけでは、Web側にも反映されたとは判断できません。
これで接続できれば、検証を無効にせずに対処できています。まだエラーになる場合は、末尾の「CAバンドルを指定しても解決しない場合」に進み、サーバー側の中間証明書や独自CAなどを確認します。今回、このダウンロード・配置・常駐プロセスの再起動自体は実行しておらず、公式資料に基づく手順です。
curl.cainfoはCURLOPT_CAINFOの既定値です。コード側でCURLOPT_CAINFOを指定している場合は、その値も調べます。openssl.cafileだけを変更してcURLへ反映されたと判断しないようにしましょう。
リクエスト単位でCAファイルを指定する例
設定をアプリケーション側で指定する場合は、次の形で確認できます。CLI用の例なので、URLと、信頼できると確認したCAファイルのパスを引数で渡します。任意の利用者からURLを受け取って実行するWeb機能としては使わないでください。
<?php
$url = $argv[1] ?? '';
$caFile = $argv[2] ?? '';
if (parse_url($url, PHP_URL_SCHEME) !== 'https' || !is_file($caFile) || !is_readable($caFile)) {
fwrite(STDERR, "HTTPS URL and a readable CA file are required.\n");
exit(1);
}
$handle = curl_init($url);
if ($handle === false) {
throw new RuntimeException('cURL initialization failed.');
}
$exitCode = 0;
try {
if (!curl_setopt_array($handle, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_CAINFO => $caFile,
])) {
throw new RuntimeException('cURL configuration failed.');
}
$body = curl_exec($handle);
if ($body === false) {
fwrite(STDERR, sprintf("cURL error %d: %s\n", curl_errno($handle), curl_error($handle)));
$exitCode = 1;
} else {
printf("HTTP %d\n", curl_getinfo($handle, CURLINFO_RESPONSE_CODE));
echo $body;
}
} finally {
curl_close($handle);
}
exit($exitCode);
tls-check.phpとして保存し、次のURLを実際の確認先に置き換えて実行します。ここでのapi.example.comは説明用の名前です。
php .\tls-check.php 'https://api.example.com/' 'C:\php-certs\cacert.pem'
このコードのTLS検証と本文取得は、前述のローカルサーバーで確認しました。実サービスへの接続や、Apache経由の実行は未確認です。また、HTTP 4xx・5xxもステータスと本文を表示する作りなので、通信できたこととAPI処理の成功は分けて判断してください。
CAバンドルを指定しても解決しない場合
CAファイルを用意すれば、どの証明書でも検証できるわけではありません。例えば、次のような点も確認します。
- サーバーが必要な中間証明書を送っているか。自分が管理するサーバーなら、サーバー側の証明書チェーンの設定を確認する。
- 社内CAや開発用CAの証明書ではないか。公開CAのバンドルに含まれない場合は、管理者など信頼できる経路でCA証明書を確認する。
- プロキシやセキュリティ製品がTLS通信を中継していないか。表示される発行者と、組織で認められた設定を確認する。
- コードや別の設定ファイルが、想定と違うCAファイルを指定していないか。
証明書の有効期限や名前の不一致は、別の確認項目です。内容を調べる方法は、OpenSSLで証明書の有効期限・SAN・発行者を確認する記事も参考にしてください。
なお、両オプションを有効にすることだけで、CAの不正発行や失効など、証明書に関するあらゆる問題へ対応できるわけではありません。失効確認などの挙動はTLSバックエンドや設定にもよります。まずは必要な検証を省かず、エラーの原因を調べて直す、という順番で対処しましょう。
参考資料
奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。
これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。
【制作会社・企業様向けサポート】
Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。
ぜひ、プロフィールやWeb制作会社様向け業務案内、一般企業様向け業務案内もご覧くださいね。
php -rの使い方:PowerShellでPHPを手軽に試す
2026.11.04
PHPファイルを作らずにコードを実行できる「php -r」の基本的な使い方を紹介します。PowerShellで変数や関数を試す例と、引用符による変数展開でエラーになる原因・対処法を説明し、Bashとの違いやPHPファイルへ切り出す目安も補足します。
PowerShellでcurlを使う:REST APIのGET・JSON POSTとエラーの確認
2026.11.03
WindowsのPowerShellからcurlでREST APIを試す方法を紹介します。GETのクエリ指定、JSONファイルのPOST、Bearer認証ヘッダーの付け方に加え、レスポンスの保存やHTTPステータス・終了コードによるエラーの切り分けを解説します。
PhpSpreadsheetでExcel読み込みのメモリ使用量を減らす:シート・列・行を絞って比較
2026.10.31
PhpSpreadsheetで必要なシート・列・行だけを読み込む方法を解説します。約18万セルのXLSXで処理時間とピークメモリを比較し、119.5MiBから16.0MiBへ削減した結果を紹介。書式を省く場合の日付判定や、読み込み範囲を絞る場合の数式への影響も確認します。
Windows版PHPのTS・NTSはどちらを選ぶ? Apacheの接続方式と確認方法
2026.10.30
Windows版PHPのThread Safe(TS)とNon Thread Safe(NTS)の違いと選び方を解説します。Apacheモジュール方式とFastCGI方式の違い、XAMPPの設定の読み方、CLIとWeb側で使われるPHP・php.iniの確認方法、DLLを選ぶ際の注意点を紹介します。