LoginSignup
1

More than 1 year has passed since last update.

posted at

updated at

PHP: SSL サイトで file_get_contents できないときは openssl の疎通確認をするとよい

Docker のコンテナ内から https の URL に対して、 curl できたのに PHP で file_get_contents できないパターンがありました。

「?」と思ったので検証してみます。

Windows 10 Pro を使用しています。

まとめ

SSL 化されたコンテンツに対する file_get_contents の可否と、 443 番ポートに関する openssl の疎通の可否が一致した。
このことから、 file_get_contentsopenssl には少なからず関連があり、 file_get_contents がうまくいかない場合に openssl での疎通確認を行うことは効果的である。

準備

まずはプロジェクトフォルダを好きな場所に作成します。

mkdir -p /path/to/dir
cd /path/to/dir

サーバーで使う証明書を用意する

今回は mkcert で証明書を作成します。
mkcert をインストールします。

私は Windows 使いなので chocolatey でインストールします。
PowerShell を管理者権限で開いて実行します。

cinst -y mkcert

初回のみ、次のコマンドを実行します。

mkcert -install

セキュリティに関する警告が出てきますが、「はい」をクリックします。

$ mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️

絵文字が出てくるのがかわいいですね :wink:

次に localhost に対する証明書を発行します。

mkdir ssl
cd ssl
mkcert localhost php-apache.com
$ mkcert localhost php-apache.com

Created a new certificate valid for the following names 📜
 - "localhost"
 - "php-apache.com"

The certificate is at "./localhost+1.pem" and the key at "./localhost+1-key.pem" ✅

It will expire on 28 February 2023 🗓

ルート証明書も後で使うので、コピーしておきます。
ルート証明書の場所は次のコマンドで確認できます。

mkcert -CAROOT

確認したら、そこから rootCA.pem をコピーします。

使用するファイルを用意する

今回用意するファイルは以下の通りです。

/path/to/dir
│  docker-compose.yml
│  Dockerfile
│
├─html
│      hello.php
│      index.php
│
└─ssl(用意済み)
        localhost+1-key.pem
        localhost+1.pem
        rootCA.pem
./docker-compose.yml

今回はコンテナ内でコマンドを打ちますので、ポートを開ける必要はありません。

./docker-compose.yml
version: '3'

services:
  php-apache:
    container_name: php-apache.com
    build: .
    volumes:
      - ./html:/var/www/html
./Dockerfile

この後の検証で何度か編集しますので、今は空のファイルを用意しておきます。

Dockerfile
./html/index.php
./html/index.php
<?php
$url = 'https://php-apache.com/hello.php';
echo file_get_contents($url);
./html/hello.php
./html/hello.php
<?php
header('Content-type: text/plain; charset=UTF-8');
echo 'hello';
exit;

これで準備は終わりです :thumbsup:

概要

ここからは実際の検証に入る前に、検証の概要について説明します。

今回はコンテナの状態を 4 つ作り、それぞれに対して 3 つの方法を試します。

コンテナの状態

  • 最初の状態: mkcert で作った証明書を配置して ssl を有効にしただけの状態
  • 状態 2: 最初の状態で、新たに CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定した状態
  • 状態 3: 最初の状態で、 CA 証明書を配置し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態
  • 状態 4: 最初の状態で、 CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態

方法

  • PHP の file_get_contents 関数で https://php-apache.com/ を読み込む
winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"

成功とする基準: PHP の Error や Warning が出ない

  • コンテナの中から curl コマンドで https://php-apache.com/ にリクエストする
winpty docker exec -it php-apache.com curl https://php-apache.com/

成功とする基準: curl に関する エラーメッセージが出ない

  • コンテナの中から openssl コマンドで php-apache.com:443 への疎通確認を行う
winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443

成功とする基準: verify error が出ない

以上の方法を行う前には、 docker-compose up -d でコンテナを立ち上げ、行った後には docker-compose down -v でコンテナを終了します。

検証

最初の状態: mkcert で作った証明書を配置して ssl を有効にしただけの状態

Dockerfile を以下のように編集します。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

file_get_contents, curl, openssl ともに失敗しました。

No. 方法 結果
1 file_get_contents 失敗
2 curl 失敗
3 openssl 失敗
詳細
  • file_get_contents

SSL に関するエラーが出ていることに注目します。

$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
PHP Warning:  file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1

Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
PHP Warning:  file_get_contents(): Failed to enable crypto in Command line code on line 1

Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
PHP Warning:  file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1

Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
  • curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=21:unable to verify the first certificate
verify return:1
...

状態 2: 最初の状態で、新たに CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定した状態

参考: [curl] HTTPS通信できない (unable to get local issuer certificate) - noknow

Dockerfile は以下の通りです。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# CA 証明書を配置する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
ENV CURL_CA_BUNDLE=/etc/ssl/certs/rootCA.pem

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

curl が成功したのに対し、
file_get_contents と openssl は失敗しました。

いわゆる curl できたのに file_get_contents できないパターン ですね。

No. 方法 結果
1 file_get_contents 失敗
2 curl 成功
3 openssl 失敗
詳細
  • file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
PHP Warning:  file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1

Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in Command line code on line 1
PHP Warning:  file_get_contents(): Failed to enable crypto in Command line code on line 1

Warning: file_get_contents(): Failed to enable crypto in Command line code on line 1
PHP Warning:  file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1

Warning: file_get_contents(https://php-apache.com/): failed to open stream: operation failed in Command line code on line 1
  • curl

PHP でエラーが起こっていますが、 curl に関してエラーが出ていない ので成功とします。

$ winpty docker exec -it php-apache.com curl https://php-apache.com/
<br />
<b>Warning</b>:  file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages:
error:1416F086:SSL routines:tls_process_server_certificate:certificate verify failed in <b>/var/www/html/index.php</b> on line <b>3</b><br />
<br />
<b>Warning</b>:  file_get_contents(): Failed to enable crypto in <b>/var/www/html/index.php</b> on line <b>3</b><br />
<br />
<b>Warning</b>:  file_get_contents(https://php-apache.com/hello.php): failed to open stream: operation failed in <b>/var/www/html/index.php</b> on line <b>3</b><br />
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify error:num=21:unable to verify the first certificate
verify return:1
...

状態 3: 最初の状態で、 CA 証明書を配置し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態

参考: [OpenSSL] [エラー] Verification error: unable to get local issuer certificate - noknow

Dockerfile は以下の通りです。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
RUN ln -s /etc/ssl/certs/rootCA.pem $(openssl version -d | cut -d' ' -f2 | sed 's/"//g')/cert.pem

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

file_get_contents と openssl が成功したのに対し、
curl は失敗しました。

No. 方法 結果
1 file_get_contents 成功
2 curl 失敗
3 openssl 成功
詳細
  • file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
hello
  • curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=1 O = mkcert development CA, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI), CN = mkcert MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
...

状態 4: 最初の状態で、 CA 証明書を配置し、環境変数 CURL_CA_BUNDLE に設定し、 OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定した状態

Dockerfile は以下の通りです。

Dockerfile
FROM php:apache-buster

RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini

# mkcert で作った証明書を配置する
COPY ./ssl/"localhost+1.pem" /etc/ssl/certs/ssl-cert-snakeoil.pem
COPY ./ssl/"localhost+1-key.pem" /etc/ssl/private/ssl-cert-snakeoil.key

# CA 証明書を配置する
COPY ./ssl/rootCA.pem /etc/ssl/certs/rootCA.pem
ENV CURL_CA_BUNDLE=/etc/ssl/certs/rootCA.pem

# OpenSSL のディレクトリに CA 証明書へのシンボリックリンクを設定する
RUN ln -s /etc/ssl/certs/rootCA.pem $(openssl version -d | cut -d' ' -f2 | sed 's/"//g')/cert.pem

# Enable SSL
RUN a2enmod ssl && a2ensite default-ssl
結果

file_get_contents, curl, openssl ともに成功しました。

No. 方法 結果
1 file_get_contents 成功
2 curl 成功
3 openssl 成功
詳細
  • file_get_contents
$ winpty docker exec -it php-apache.com php -r "echo file_get_contents('https://php-apache.com/'), PHP_EOL;"
hello
  • curl
$ winpty docker exec -it php-apache.com curl https://php-apache.com/
hello
  • openssl
$ winpty docker exec -it php-apache.com openssl s_client -quiet -connect php-apache.com:443
depth=1 O = mkcert development CA, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI), CN = mkcert MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
depth=0 O = mkcert development certificate, OU = MYCOMPUTER\\norit@MyComputer (Noritaka IZUMI)
verify return:1
...

最終結果と考察

あらためてそれぞれの状態での結果を一つの表にまとめてみると、file_get_contents と openssl で結果が一致していることがわかります :bulb:

No. 方法 最初の状態 状態 2 状態 3 状態 4
1 file_get_contents 失敗 失敗 成功 成功
2 curl 失敗 成功 失敗 成功
3 openssl 失敗 失敗 成功 成功

OS や PHP, Apache の状態によっては結果が変わる可能性もあるため、一概に file_get_contents と openssl の疎通可否を必要十分条件ということはできませんが、少なくともある程度有益な情報が得られたとは思います :thinking:

もし、同じエラーに詰まった方がいらっしゃいましたら、参考にしていただけると幸いです :wink:

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1