php / php/php-src

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

Open
#20,586 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Bug Extension: openssl Status: Needs Triage
Dominant language
C
Stars
40.4k
Forks
8.1k
Avg merge
2d 13h
Merged PRs (30d)
96

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in ext/openssl/openssl_backend_common.c at php_openssl_setup_verify() and the check_cert() path. Reproduce the openssl_x509_checkpurpose() example with file-only, directory-only, and mixed ca_info values. Done means valid user-provided CA files or directories prevent the system CA store from being partially loaded.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, php
Domain
backend, security
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.