Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

curl: Fallback to using Certificate Authority from Windows Certificate Store #13601

Open
Luc45 opened this issue Mar 5, 2024 · 4 comments · Fixed by woocommerce/qit-cli#138

Comments

@Luc45
Copy link

Luc45 commented Mar 5, 2024

Description

In PHP's current implementation, there is an inconsistency in how the language handles SSL/TLS certificate validation across different operating systems. This inconsistency primarily affects Windows users.

In this proof-of-concept, we can see the behavior clearly:

On Windows, the script does two requests:

  1. The first request without a CA file that fails and prints Request correctly failed without CA certificate (expected on Windows)
  2. A second request using the CA file from Mozilla that succeeds and prints Request succeeded with provided CA certificate

On Linux and Mac, it uses the O.S CA file, which prints Request succeeded without CA certificate (expected on Linux and macOS)

On Linux, it uses by default:

CAfile: /etc/ssl/certs/ca-certificates.crt
CApath: /etc/ssl/certs

On Mac, it uses:

CAPath: /usr/local/etc/openssl@3/certs

On Windows, both are blank:

CAfile: (empty)
CApath: (empty)

Which means that PHP doesn't have a fallback Certificate Authority file to validate HTTPS requests, which leads to issues such as these. The common solution for this problem for Windows users is to download a trusted CA file (such as from Mozilla), and update php.ini to use it:

This is a sub-optimal solution as it increases complexity for the average John Doe that just wants to do a network request against a HTTPS URL.

I think PHP in Windows could fallback to accessing the Windows Certificate Store, or bundle a trusted CA file, although this might get outdated.

@Luc45 Luc45 changed the title Fallback from using Certificate Authority from Windows Certificate Store Fallback to using Certificate Authority from Windows Certificate Store Mar 5, 2024
@Luc45 Luc45 reopened this Mar 5, 2024
@Ayesh
Copy link
Member

Ayesh commented Mar 11, 2024

Unlike Macos and Linux, Windows doesn't have a single file root CA file. That's why the ini values are empty in the first place.

We recently upped the minimum required Libcurl version to 7.61.0. There is support to use native Windows CA store in Libcurl 7.71 (https://curl.se/libcurl/c/CURLOPT_SSL_OPTIONS.html). It will need some work in the Curl extension, but I think this is the way to go. Probably easier to implement now with a not-so-distant minimum required version.

One alternative, bundling our own root CA list, is a big no-no in my opinion.

@theodorejb
Copy link
Contributor

Could the minimum libcurl version be bumped to at least 7.71.0 (released in 2020) for PHP 8.5? It would be really nice to reduce the amount of setup/config needed to use the cURL extension on Windows.

@Tracy-B
Copy link

Tracy-B commented Mar 25, 2025

One appliation I use is SimpleSAMLphp, which uses [PHPMailer] for sending emails (https://github.com/PHPMailer/PHPMailer), which uses the function stream_socket_enable_crypto.

Is updating libcurl going to solve the problems for the likes of PHPMailer, or is it only a partial solution, with the full soultion being to look into the window certificate store?

@bukka
Copy link
Member

bukka commented Mar 29, 2025

The stream_socket_enable_crypto is for PHP streams that have Windows Certificate Storea fallback. See

#ifdef PHP_WIN32
#define RETURN_CERT_VERIFY_FAILURE(code) X509_STORE_CTX_set_error(x509_store_ctx, code); return 0;
static int php_openssl_win_cert_verify_callback(X509_STORE_CTX *x509_store_ctx, void *arg) /* {{{ */
{
PCCERT_CONTEXT cert_ctx = NULL;
PCCERT_CHAIN_CONTEXT cert_chain_ctx = NULL;
X509 *cert = X509_STORE_CTX_get0_cert(x509_store_ctx);
php_stream *stream;
php_openssl_netstream_data_t *sslsock;
zval *val;
bool is_self_signed = 0;
stream = (php_stream*)arg;
sslsock = (php_openssl_netstream_data_t*)stream->abstract;
{ /* First convert the x509 struct back to a DER encoded buffer and let Windows decode it into a form it can work with */
unsigned char *der_buf = NULL;
int der_len;
der_len = i2d_X509(cert, &der_buf);
if (der_len < 0) {
unsigned long err_code, e;
char err_buf[512];
while ((e = ERR_get_error()) != 0) {
err_code = e;
}
php_error_docref(NULL, E_WARNING, "Error encoding X509 certificate: %lu: %s", err_code, ERR_error_string(err_code, err_buf));
RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED);
}
cert_ctx = CertCreateCertificateContext(X509_ASN_ENCODING, der_buf, der_len);
OPENSSL_free(der_buf);
if (cert_ctx == NULL) {
char *err = php_win_err();
php_error_docref(NULL, E_WARNING, "Error creating certificate context: %s", err);
php_win_err_free(err);
RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED);
}
}
{ /* Next fetch the relevant cert chain from the store */
CERT_ENHKEY_USAGE enhkey_usage = {0};
CERT_USAGE_MATCH cert_usage = {0};
CERT_CHAIN_PARA chain_params = {sizeof(CERT_CHAIN_PARA)};
LPSTR usages[] = {szOID_PKIX_KP_SERVER_AUTH, szOID_SERVER_GATED_CRYPTO, szOID_SGC_NETSCAPE};
DWORD chain_flags = 0;
unsigned long allowed_depth = OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH;
unsigned int i;
enhkey_usage.cUsageIdentifier = 3;
enhkey_usage.rgpszUsageIdentifier = usages;
cert_usage.dwType = USAGE_MATCH_TYPE_OR;
cert_usage.Usage = enhkey_usage;
chain_params.RequestedUsage = cert_usage;
chain_flags = CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_CHAIN_EXCLUDE_ROOT;
if (!CertGetCertificateChain(NULL, cert_ctx, NULL, NULL, &chain_params, chain_flags, NULL, &cert_chain_ctx)) {
char *err = php_win_err();
php_error_docref(NULL, E_WARNING, "Error getting certificate chain: %s", err);
php_win_err_free(err);
CertFreeCertificateContext(cert_ctx);
RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED);
}
/* check if the cert is self-signed */
if (cert_chain_ctx->cChain > 0 && cert_chain_ctx->rgpChain[0]->cElement > 0
&& (cert_chain_ctx->rgpChain[0]->rgpElement[0]->TrustStatus.dwInfoStatus & CERT_TRUST_IS_SELF_SIGNED) != 0) {
is_self_signed = 1;
}
/* check the depth */
GET_VER_OPT_LONG("verify_depth", allowed_depth);
for (i = 0; i < cert_chain_ctx->cChain; i++) {
if (cert_chain_ctx->rgpChain[i]->cElement > allowed_depth) {
CertFreeCertificateChain(cert_chain_ctx);
CertFreeCertificateContext(cert_ctx);
RETURN_CERT_VERIFY_FAILURE(X509_V_ERR_CERT_CHAIN_TOO_LONG);
}
}
}
{ /* Then verify it against a policy */
SSL_EXTRA_CERT_CHAIN_POLICY_PARA ssl_policy_params = {{sizeof(SSL_EXTRA_CERT_CHAIN_POLICY_PARA)}};
CERT_CHAIN_POLICY_PARA chain_policy_params = {sizeof(CERT_CHAIN_POLICY_PARA)};
CERT_CHAIN_POLICY_STATUS chain_policy_status = {sizeof(CERT_CHAIN_POLICY_STATUS)};
BOOL verify_result;
ssl_policy_params.dwAuthType = (sslsock->is_client) ? AUTHTYPE_SERVER : AUTHTYPE_CLIENT;
/* we validate the name ourselves using the peer_name
ctx option, so no need to use a server name here */
ssl_policy_params.pwszServerName = NULL;
chain_policy_params.pvExtraPolicyPara = &ssl_policy_params;
verify_result = CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, cert_chain_ctx, &chain_policy_params, &chain_policy_status);
CertFreeCertificateChain(cert_chain_ctx);
CertFreeCertificateContext(cert_ctx);
if (!verify_result) {
char *err = php_win_err();
php_error_docref(NULL, E_WARNING, "Error verifying certificate chain policy: %s", err);
php_win_err_free(err);
RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED);
}
if (chain_policy_status.dwError != 0) {
/* The chain does not match the policy */
if (is_self_signed && chain_policy_status.dwError == CERT_E_UNTRUSTEDROOT
&& GET_VER_OPT("allow_self_signed") && zend_is_true(val)) {
/* allow self-signed certs */
X509_STORE_CTX_set_error(x509_store_ctx, X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT);
} else {
RETURN_CERT_VERIFY_FAILURE(SSL_R_CERTIFICATE_VERIFY_FAILED);
}
}
}
return 1;
}
/* }}} */
#endif
. If you see issue with that, please open a new issue. This should be just for curl.

@bukka bukka changed the title Fallback to using Certificate Authority from Windows Certificate Store curl: Fallback to using Certificate Authority from Windows Certificate Store Mar 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants