SSL-Verifizierung mit PHP schlägt fehlt (Fehler 14090086)

Keywords: #certificate-chain #curl #intermediate #php #ssl #zertifikate #zertifikatskette

Wenn dich PHP mit dieser Fehlermeldung (oder einer ähnlichen Fehlernummer) begrüßt, dann liegt es wohl daran, dass beim Abruf einer SSL-Ressource die Identität nicht verifiziert werden konnte. Und das ist auch gut so, denn der Sinn von SSL ist ja das Herstellen einer gesicherten Verbindung.
So sieht die Fehlermeldung bei Verwendung von file_get_contents(); aus. Aber auch andere Funktionen, mit denen man auf externe Ressourcen verweisen kann, werfen diesen Fehler, wie z.B. imagecreatefrompng();.

Warning: file_get_contents(): SSL operation failed with code 1. OpenSSL Error messages: error:14090086:SSL routines:ssl3_get_server_certificate:certificate verify failed in /website/script.php on line 50

Warning: file_get_contents(): Failed to enable crypto in /website/script.php
on line 50

Warning: file_get_contents(https://www.nickyreinert.de/foobar.json): failed to open stream: operation failed in /website/script.php
on line 50

Die Erklärung ist simpel: PHP kennt den Aussteller des Zertifikates für https://www.nickyreinert.de nicht und verweigert aus Sicherheitsgründen den Aufbau einer Verbindung. Die Ursachen hingegen können vielfältig sein, oft ist der Aussteller des Zertifikats z.B. nicht bekannt. Wir müssen also entweder dafür sorgen, dass PHP dem Aussteller vertraut oder festlegen, dass PHP die Zertifikate gar nicht erst prüft.

Verifizierung des SSL Hosts unterdrücken

Das ist die quick’n’dirty Lösung. Zumindest file_get_contents(); kann mit Parametern gefüttert werden, die die Verifizierung der Ressource unterdrücken:

$stream = stream_context_create(array(     $stream = stream_context_create(array( “ssl”=>array(      “verify_peer”=> false,     “verify_peer_name”=> false, ), ‘http’ => array(     ’timeout’ => 30     ) )     );

$result = file_get_contents($url, 0, $stream);

Wer auf eine eigene Ressource zurückgreift, z.B. im lokalen Netz oder zu Testzwecken, kann damit leben. Aus Sicherheitsgründen ist die Lösung allerdings nicht zu empfehlen. Außerdem erlaubt nicht jede Funktion die Angabe eines Stream-Contextes, wie z.B. imagecreatefromjpeg();

Die Zertifikatskette manuell einrichten

Man kommt also nicht darum, die saubere Lösung zu nutzen. Dazu muss man wissen, dass es in den allermeisten Fällen nicht nur um das eine Zertifikat geht, sondern um die gesamte Zertifikatskette. Diese beinhaltet auch die Zertifikate der Stellen, die dem infragekommenden Server, also https://www.nickyreinert.de, das Zertifikat ausgestellt haben. Wenn PHP diese Stellen nicht kennt, geht es auch davon aus, dass die von dort ausgestellten Zertifikate nicht gültig sind.

 SSL Zertifikats-Kette mit den Intermediate Zertifikaten

SSL Zertifikats-Kette mit den Intermediate Zertifikaten

Um die Zertifikatskette zu erhalten, kannst du einen Service wie https://whatsmychaincert.com/ nutzen. Dieser liefert dir eine Datei mit der  kompletten Zertifikatskette. Oder du machst dich selber auf die Suche. Jedes Zertfikat enthält den Namen des Ausstellers bzw. den Typ des Zertifikats. Damit lassen sich die notwendigen Zwischenzertifkate auch manuell zusammenstellen.

PHP die Zertifikatskette mitteilen

Die Datei mit der Zertifikatskette gehört nun, oh Wunder, an einen Ort, den PHP erreichen kann. Dann musst du PHP noch mitteilen, dass es auch diese Zertifikatskette berücksichtigen soll. Auch das passiert über den Stream-Context, den ich oben schon angesprochen habe. Doch diesmal erlauben wir PHP, den SSL-Host zu verfizieren und verweisen auf die Zertifikatskette, die wir oben erstellt haben:

$stream = stream_context_create(array( “ssl”=>array(     “cafile” => “www.nickyreinert.de.pem”,     “verify_peer”=> true,     “verify_peer_name”=> true, ),‘http’ => array(     ’timeout’ => $this->configUrlTimeOut     ) ) ; $config = file_get_contents($url, 0, $stream);

Geschafft. PHP sollte nun, zur Laufzeit, den SSL-Host überprüfen und dabei auf die Zertifikate zurückgreifen, die die Authentizität einwandfrei bestätigen.

Man kann den Verweis auch an anderer Stelle global definieren. Die PHP-Funktion openssl_get_cert_locations(); teilt uns direkt mit, wo PHP nach gültigen Zertifikaten sucht:

Array ( [default_cert_file] => /Applications/XAMPP/xamppfiles/share/openssl/cert.pem [default_cert_file_env] => SSL_CERT_FILE [default_cert_dir] => /Applications/XAMPP/xamppfiles/share/openssl/certs [default_cert_dir_env] => SSL_CERT_DIR [default_private_dir] => /Applications/XAMPP/xamppfiles/share/openssl/private [default_default_cert_area] => /Applications/XAMPP/xamppfiles/share/openssl [ini_cafile] => /Applications/XAMPP/xamppfiles/share/curl/curl-ca-bundle.crt [ini_capath] => )

Der entscheidende Parameter lautet ini_cafile. Diese wird in der php.ini mit dem Parameter

openssl.cafile=/Applications/XAMPP/xamppfiles/share/curl/curl-ca-bundle.crt

gesetzt. Die gleiche Info erhältst du auch über die bekannte Funktion phpinfo(), dort lautet der Parameter openssl.cafile.

In der entsprechenden Datei liegen eine Menge von Root-Zertifikaten, bzw. Certificate-Authorities. Sprich die Aussteller, denen PHP grundsätzlich vertraut. Mitunter macht es mehr Sinn, diese Einstellung in der php.ini zu nutzen, damit auch curl() darauf zurückgreifen kann.

Wenn du PHP-FPM benutzt, musst du den Parameter übrigens folgendermaßen ansprechen:

php_admin_value[openssl.cafile] = /Applications/XAMPP/xamppfiles/share/curl/curl-ca-bundle.crt

Zusammenfassung

Die SSL-Fehlermeldung hat ihren Sinn. PHP ist nicht in der Lage, die Authentizität des Servers zu überprüfen. Der korrekte Weg das Problem zu beheben, ist das Zertifikat der entsprechenden Seite herunterzuladen, die Intermediate-Zertifikate und das Root-Zertifikat zu besorgen und alle Zertifikate im PEM-Format in eine Text-Datei zu packen.
Danach kannst du entweder auf die URL zugreifen und über den Stream-Kontext auf diese Datei mit der Zertifikats-Kette verweisen, oder du legst in den globalen PHP-Einstellungen fest, dass PHP diese Datei beim nächsten Mal berücksichtigen soll.