php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #49419 ssl:// wrapper - cannot verify VeriSign certificate chain
Submitted: 2009-08-30 18:46 UTC Modified: 2009-10-22 17:13 UTC
Votes:2
Avg. Score:3.5 ± 0.5
Reproduced:2 of 2 (100.0%)
Same Version:0 (0.0%)
Same OS:1 (50.0%)
From: Jacek at jacekk dot info Assigned:
Status: Not a bug Package: OpenSSL related
PHP Version: 5.3.0 OS: Ubuntu
Private report: No CVE-ID: None
 [2009-08-30 18:46 UTC] Jacek at jacekk dot info
Description:
------------
PHP cannot validate some (VeriSign's?) certificate chains correctly. openssl s_client works fine with the same input.

Verification of thawte chain works well.

chain.pem is available at http://pastebin.com/f4ab25a9a

OpenSSL:
$ openssl s_client -connect www.verisign.com:443 -CAfile chain.pem
(...)
    Verify return code: 0 (ok)
(...)

Reproduce code:
---------------
<?php
$ssl = array(
        'verify_peer' => TRUE,
        'verify_depth' => 5,
        'allow_self_signed' => FALSE,
        'cafile' => 'chain.pem',
        'capture_peer_cert' => TRUE,
        'capture_peer_chain' => TRUE,
);
$context = stream_context_create(array(
        'ssl' => $ssl,
));

file_get_contents('https://api-3t.paypal.com/', NULL, $context);
file_get_contents('https://www.verisign.com/', NULL, $context);
?>

Expected result:
----------------
Nothing

Actual result:
--------------
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 /home/me/test/test.php on line 14

Warning: file_get_contents(): Failed to enable crypto in /home/me/test/test.php on line 14

Warning: file_get_contents(https://api-3t.paypal.com/): failed to open stream: operation failed in /home/me/test/test.php on line 14

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 /home/me/test/test.php on line 15

Warning: file_get_contents(): Failed to enable crypto in /home/me/test/test.php on line 15

Warning: file_get_contents(https://www.verisign.com/): failed to open stream: operation failed in /home/me/test/test.php on line 15


Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2009-09-04 06:59 UTC] ryan+phpbugs at sleevi dot com
I was unable to reproduce the "good" OpenSSL output that you described, using OpenSSL FIPS 1.2. For documentation sake (and because everything I'm about to explain is relative to that, which is equivalent to 0.9.8f code more or less), what version have you linked against with your PHP?

Running

openssl s_client -connect www.verisign.com:443 -CAfile chain.pem

I get

CONNECTED(00000003)
depth=3 /C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
verify error:num=19:self signed certificate in certificate chain
verify return:0
---
Certificate chain
 0 s:/1.3.6.1.4.1.311.60.2.1.3=US/1.3.6.1.4.1.311.60.2.1.2=Delaware/2.5.4.15=V1.0, Clause 5.(b)/serialNumber=2497886/C=US/postalCode=94043/ST=California/L=Mountain View/streetAddress=487 East Middlefield Road/O=VeriSign, Inc./OU=Production Security Services/CN=www.verisign.com
   i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)06/CN=VeriSign Class 3 Extended Validation SSL SGC CA
 1 s:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)06/CN=VeriSign Class 3 Extended Validation SSL SGC CA
   i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5
 2 s:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5
   i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
 3 s:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
   i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
---

Using the chain file at http://pastebin.com/f16f72c6e and I am able to use the code without the issues you describe (There is an HTTP 500 error with the PayPal site, but that demonstrates the connection is successful). The certificate in this file corresponds to the last certificate in the chain as supplied by both Verisign and Paypal.

The explanation of "why" this works follows, and is based on the 0.9.8/OpenSSL FIPS Module 1.2 code. While I cannot be certain that this explanation fully explains your problem, based on those missing pieces of information, it may shed light on the issues and limitations of the 'cafile' and 'capath' arguments.

I do not believe that what you want to work will work the way you've described, which is due to OpenSSL limitations.

In the chain file you provided, the two certificates you provided are:
subject= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5
issuer= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5

and

subject= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)06/CN=VeriSign Class 3 Extended Validation SSL CA
issuer= /C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. - For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority - G5


The first certificate in the supplied chain file corresponds to being the issuer of the certificate at index 1 in the above output from OpenSSL. Thus, one would expect the chain to go from index 0 (www.verisign.com) -> index 1 (EV SSL SGC CA) -> chain file 0 (Public Primary CA - G5). As chain file 0 is both a self-signed certificate and in the trusted list, one would presume the connection is now trusted/verified.

However, OpenSSL's verify code is set to respect the ordering supplied by the remote server over the preference of a chain file when used for certificate path building. The untrusted list of certs receives precedence when building the chain, with the capath, cafile, and any other lookups only being consulted to complete any missing parts of the chain.

This can be found in the OpenSSL sources in crypto/x509/x509_vfy.c X509_verify_cert. The comment in the code states "if we were passed a cert chain, use it first", in reference to the 'untrusted' chain.

The Verisign server, above, is supplying a chain that actually terminates at "Class 3 Public Primary Certification Authority" (Index 3) This is the certificate that would need to be contained in chain.pem for verification to happen properly, as this is the certificate that OpenSSL (ergo, by proxy, PHP) expects to find in the trusted store (created by the cafile/capath options).

If the Verisign server were instead supplying the (intermediate) certificate "CN=VeriSign Class 3 Public Primary Certification Authority - G5" (Index: 2) as the last certificate, then the chain.pem you supplied would still not work, because the Verisign server is supplying a version who says its issuer is the "Class 3 Public Primary Certification Authority". As such, that is the certificate that would be looked up, rather than the self-signed version that you have in your chain.pem.

If the Verisign server was supplying the last certificate as "CN=VeriSign Class 3 Extended Validation SSL SGC CA" (Index 1), then the code you supplied *WOULD* work for Verisign, because OpenSSL would have an incomplete chain, and then examine chain.pem to inject the trusted (self-signed) version of the G5 key.

For the PayPal example, the chain I receive from

openssl s_client -connect www.paypal.com:443

is

Certificate chain
 0 s:/C=US/ST=California/L=San Jose/O=PayPal, Inc./OU=Information Systems/CN=api-3t.paypal.com
   i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)05/CN=VeriSign Class 3 Secure Server CA
 1 s:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)05/CN=VeriSign Class 3 Secure Server CA
   i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority

Here, as best I can tell, none of the certificates in your chain.pem match certificates in this list. However, using the chain.pem I supplied above, which contains the "Class 3 Public Primary Certification Authority", the code executes as intended.
 [2009-09-04 17:21 UTC] Jacek at jacekk dot info
> what version have you linked against with your PHP?

I've tested it on PHP linked against OpenSSL 0.9.8g (Ubuntu LTS) and 0.9.8k (Gentoo)

I understand how certificate checking works and I've provided wrong chain - my bad, however with the file you linked PHP still shows warnings form the first message.

One more question: must OpenSSL check whole path, if one of intermediate certificates exists in cafile?
 [2009-09-13 13:53 UTC] ryan+phpbugs at sleevi dot com
As mentioned previously, the PayPal URL returns HTTP 500. This is not an SSL error, it is an error in the URL being requested. However, the fact that you get that error indicates OpenSSL is working correctly.

Verifying a certificate necessarily involves verifying its' entire chain. Begin with a simple chain where A issues B issues C. C is the certificate the web server is using. To make sure that C is authorized, you must consult C to see if it's issuer has placed any restrictions/constraints on it. However, restrictions are inherited properties, so you must *ALSO* check both B and A to make sure no restrictions are placed. What C is allowed to do is the combined, least permissive set of A, B, and C. This is spelled out in the various PKI/PKIX RFCs.

OpenSSL, as best I can tell, does not integrate alternative certificate path building algorithms when multiple paths may exist. That was your problem with Verisign, the server supplies one chain, while you were expecting/trusting a different chain path. This can be implemented with the OpenSSL verify callback, but not directly in PHP. At that point, it becomes more of a feature request, then a bug.

In the matter of trusting intermediates, OpenSSL 0.9.8 makes specific mention in several places throughout x509_vfy.c (the code which contains the verification routines), that it only attempts to complete the chain at the last untrusted certificate. Per the TLS specification, a server sends its cert, and additionally any supplementary/intermediate certificates it believes clients' will need to build a chain.

To visualize, think of a chain that goes A B C D E F, where F is the server certificate and A is the root certificate. When connecting via TLS, the server may supply:

F
F E
F E D

etc.

In OpenSSL, and therefore in PHP, it expects to try to verify the last certificate (and the remaining chain). If you connect to a server that only supplies F, then you need to have E-A in your certificate store. If a server supplies F E D, then you need to have C-A in your certificate store. And if a server supplies F-A (such as Verisign does), then you only need to have A in your store.

This is a very simple model for path building that OpenSSL supports in-code (though it provides the means to support other methods via the necessary callbacks, I believe). RFC 4158 provides some of the complexities involved with path building and the various structures that may be represented.

All of that to say that, in today's world and with today's code, yes, you are best supplying all of the intermediates that you trust, in addition to the root certificates, as the servers you are connecting to may not all be configured to send them, and trust will fail otherwise. Further, in today's code, simply trusting the intermediate is often not enough to successfully pass verification.
 [2009-10-22 17:13 UTC] Jacek at jacekk dot info
Ok, bogus.
 [2015-09-29 14:35 UTC] gamphd at verizon dot net
I don't _want_ the certificate checked.  I want the Windows version to behave as predictably as the Ubuntu version, which I realize is asking a lot.  I want to debug encrypted responses without the limitations of tcpdump and ssldumt, but there's no obvious php.ini entry to do that.  For that matter, I got the desired behavior of understanding tls:// _without_ modifying php.ini.
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Wed Apr 24 10:01:31 2024 UTC