Skip to content

check_cert() uses system CA store even if user provided own cert to be used as CA #20586

@arekm

Description

@arekm

Description

I was trying to figure out why this code

<?php
$cert = file_get_contents("single-letsencrypt-domain-cert.pem")
print_r(openssl_x509_checkpurpose($cert, X509_PURPOSE_SSL_SERVER, array('letsencrypt-intermediate.pem')));

worked (returned True) on some systems while on other returned False. Turns out the key difference is that one system had CA certificates in a bundle ONLY:

/etc/pki/tls/certs/ca-bundle.crt

while the other system had above bundle BUT also individual hashed CA certs in /etc/openssl/certs/ directory.

/etc/pki/tls/certs/ca-bundle.crt
/etc/openssl/certs/*.0 (bunch of hashes as symlinks to actual individual CA cert files)

(note this is on PLD/Linux, so on your system layout can be different)

Now openssl_x509_checkpurpose() third parameter is "ca_info should be an array of trusted CA files/dirs as described in Certificate Verification."

I assume the goal is to override system CA certs. (My assumption is also based on how/when check_cert() tries to load system CA certs)

openssl_x509_checkpurpose() then uses check_cert() to load CA certs.

Unfortunately the code is like this

[code from php 8.5 git]

parse what user has specified in ca_info (calist), if some files then "nfiles++", if dirs then "ndirs++",
load what user specified (if possible) and then handle system CA certs but do it in such way:


 832     if (nfiles == 0) {
 833         file_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
 834         if (file_lookup == NULL || !X509_LOOKUP_load_file(file_lookup, NULL, X509_FILETYPE_DEFAULT)) {
 835             php_openssl_store_errors();
 836         }
 837     }
 838     if (ndirs == 0) {
 839         dir_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir());
 840         if (dir_lookup == NULL || !X509_LOOKUP_add_dir(dir_lookup, NULL, X509_FILETYPE_DEFAULT)) {
 841             php_openssl_store_errors();
 842         }
 843     }

Which means:

  • if user specified (in ca_info) any files and only files then sytem CA files won't be loaded BUT sytem directories will (due to ndirs==0)!
  • if user specified (in ca_info) any directories and only directories then sytem CA directories won't be loaded BUT sytem files will (due to nfiles==0)!
  • if user specified (in ca_info) any files and any directories then no CA files and no CA directories will be used (due to ndirs > 0 and nfiles > 0).

That makes no sense. Partial system CA is getting kind of "randomly" loaded only based on file vs directory type.

I would expect only user provided CA files/dirs to be used and no system CA store if user uses anything valid as ca_info parameter of openssl_x509_checkpurpose()

So the fix - use system CA certs in form of files and directories only if user didn't override these with own ca_info (calist) parameter.

diff --git a/ext/openssl/openssl_backend_common.c b/ext/openssl/openssl_backend_common.c
index c21e64a1306..1f4509f40f0 100644
--- a/ext/openssl/openssl_backend_common.c
+++ b/ext/openssl/openssl_backend_common.c
@@ -829,13 +829,11 @@ X509_STORE *php_openssl_setup_verify(zval *calist, uint32_t arg_num)
                        }
                } ZEND_HASH_FOREACH_END();
        }
-       if (nfiles == 0) {
+       if (nfiles == 0 && ndirs == 0) {
                file_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file());
                if (file_lookup == NULL || !X509_LOOKUP_load_file(file_lookup, NULL, X509_FILETYPE_DEFAULT)) {
                        php_openssl_store_errors();
                }
-       }
-       if (ndirs == 0) {
                dir_lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir());
                if (dir_lookup == NULL || !X509_LOOKUP_add_dir(dir_lookup, NULL, X509_FILETYPE_DEFAULT)) {
                        php_openssl_store_errors();

PHP Version

$ php84 --version
PHP 8.4.13 (cli) (built: Sep 27 2025 01:01:24) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.13, Copyright (c) Zend Technologies

(tests were done on this php)

Operating System

PLD/Linux current Th

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions