Few things break a developer's flow faster than a cryptic environment error. You are building a feature that relies on an external API—perhaps a payment gateway like Stripe or a simple fetch request to GitHub. You write the code, run it locally, and are immediately blocked by this error message:
cURL error 60: SSL certificate problem: unable to get local issuer certificate
If you are using XAMPP or WAMP on Windows, this is a rite of passage. While the quick search result often suggests disabling SSL verification in your code, do not do this. That exposes your application to Man-in-the-Middle (MITM) attacks.
This guide provides the industry-standard solution to fix the root cause in your php.ini configuration, ensuring your local environment is as secure as production.
The Root Cause: The Missing Chain of Trust
To understand the fix, you must understand the failure. When your PHP application makes a request to https://api.example.com using cURL, an SSL handshake occurs. The remote server presents its SSL certificate to prove its identity.
For this handshake to succeed, your local cURL client needs to verify that the server's certificate was issued by a trusted authority (like DigiCert or Let's Encrypt). It does this by checking the server's certificate against a local CA Bundle (Certificate Authority Bundle)—a list of trusted issuers.
Here is the problem: Linux environments usually share a centralized CA bundle provided by the OS. However, Windows environments (specifically portable stacks like XAMPP) do not automatically map to the Windows system certificate store. Out of the box, XAMPP's PHP configuration often points to an empty or non-existent file path for this bundle.
Because cURL cannot find the list of trusted issuers, it assumes every SSL connection is untrusted and throws Error 60.
Step-by-Step Solution
We will download a valid CA bundle and configure PHP to use it globally. This fixes the issue for cURL, file_get_contents, and Guzzle.
1. Download the cacert.pem Bundle
The cURL project maintains an extracted version of the Mozilla CA bundle. This is the industry standard for PHP development.
- Visit the official cURL website: https://curl.se/docs/caextract.html.
- Download the
cacert.pemfile linked at the top of the page.
2. Place the File in Your PHP Directory
You need to save this file in a permanent location where PHP can read it. Avoid placing it in temporary folders or your desktop.
If you are using XAMPP, the standard PHP directory is usually C:\xampp\php.
- Navigate to
C:\xampp\php. - Create a new folder named
extras(if it doesn't exist), and inside that, a folder namedssl. - Move your downloaded
cacert.pemfile here. - Your path should look like this:
C:\xampp\php\extras\ssl\cacert.pem
3. Edit Your php.ini Configuration
Now we need to tell PHP where to find this file.
- Open your XAMPP Control Panel.
- Click the Config button next to Apache and select PHP (php.ini). Alternatively, locate the file manually at
C:\xampp\php\php.ini. - Press
Ctrl+Fto search. Look forcurl.cainfo.
You will likely find a commented-out line (starting with ;). Remove the semicolon and update the path to point to your new file. You should also update the openssl.cafile setting to ensure non-cURL streams work correctly.
Add or modify these lines:
[curl]
; A default value for the CURLOPT_CAINFO option.
; This is required to be an absolute path.
curl.cainfo = "C:\xampp\php\extras\ssl\cacert.pem"
[openssl]
; The location of a Certificate Authority (CA) file on the local filesystem
openssl.cafile = "C:\xampp\php\extras\ssl\cacert.pem"
Note on File Paths: Windows supports both forward slashes (/) and backslashes (\) in php.ini, but using double quotes around the path is a best practice to avoid parsing errors with spaces.
4. Restart Apache
This is the most common point of failure. PHP loads its configuration only when the server starts.
- Go to the XAMPP Control Panel.
- Click Stop next to Apache.
- Wait for the ports to clear.
- Click Start.
Verifying the Fix
Do not assume it works; verify it with code. Create a file named test_ssl.php in your htdocs folder and run the following script.
This script uses modern PHP practices to check your configuration and attempt a live connection to GitHub's API (which requires strict SSL).
<?php
/**
* SSL Configuration Diagnostic Script
* Checks php.ini settings and attempts a real SSL handshake.
*/
// 1. Check if the path is configured in php.ini
$curlPath = ini_get('curl.cainfo');
$opensslPath = ini_get('openssl.cafile');
echo "<h2>Configuration Check</h2>";
echo "<strong>curl.cainfo:</strong> " . ($curlPath ?: 'Not Set') . "<br>";
echo "<strong>openssl.cafile:</strong> " . ($opensslPath ?: 'Not Set') . "<br>";
// 2. Verify the file actually exists
if ($curlPath && file_exists($curlPath)) {
echo "<span style='color:green'>✅ Certificate file found.</span><br>";
} else {
echo "<span style='color:red'>❌ Certificate file NOT found. Check your path!</span><br>";
}
// 3. Attempt a real request
echo "<h2>Connection Test</h2>";
$url = "https://api.github.com/zen"; // GitHub requires User-Agent and SSL
$ch = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => 'Local-Dev-Test', // GitHub requires this
CURLOPT_CONNECTTIMEOUT => 10,
// We are NOT disabling SSL verification here.
// If this works, your system is secure.
];
curl_setopt_array($ch, $options);
$response = curl_exec($ch);
$error = curl_error($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if ($error) {
echo "<div style='background:#fee; padding:10px; border:1px solid red;'>";
echo "<strong>Failed:</strong> " . htmlspecialchars($error);
echo "</div>";
} else {
echo "<div style='background:#eef; padding:10px; border:1px solid green;'>";
echo "<strong>Success!</strong> HTTP Status: " . $info['http_code'] . "<br>";
echo "<strong>Response:</strong> " . htmlspecialchars($response);
echo "</div>";
}
?>
If you see "Success!" and a Zen message from GitHub, your environment is correctly configured.
Troubleshooting Common Pitfalls
Even with the correct steps, you might hit edge cases. Here are the technical nuances to watch for.
The "Multiple php.ini" Trap
XAMPP and WAMP installs can sometimes have multiple configuration files (e.g., one for CLI and one for Apache).
Run phpinfo() in a browser script. Look for the row labeled "Loaded Configuration File". Ensure you edited the file listed there. If you are running PHP from the command line (Terminal/PowerShell), run php --ini to verify which file the CLI is using.
The Relative Path Error
Never use relative paths (e.g., ..\extras\ssl\cacert.pem) in php.ini. The working directory of PHP changes depending on whether it is executed by Apache or the CLI. Always use the absolute drive path (e.g., C:\...).
Why Not Just Disable SSL?
You might see StackOverflow answers suggesting this snippet:
// ⚠️ DANGEROUS CODE - DO NOT USE IN PRODUCTION
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
While this removes the error, it blinds your application. If you use this locally, you might accidentally commit it to production. Furthermore, if you are developing a payment integration, the SDKs (like Stripe-PHP) will often force SSL verification on, meaning the "hack" won't work within the library anyway.
Conclusion
The cURL error 60 is not a bug in your code; it is a configuration gap in the Windows development stack. By downloading the official cacert.pem bundle and explicitly defining it in your php.ini, you create a robust, production-mirroring environment.
This ensures that when your code moves from local to live, it continues to work securely without requiring dangerous code modifications.