Passing client certificates through Curl request using Guzzle
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
# Passing Client Certificates Through Guzzle Requests: Solving the SSL Handshake Failure
As senior developers working with HTTP clients in PHP, we often encounter situations where command-line tools work perfectly, but when porting that functionality to an abstraction layer like Guzzle, subtle configuration differences lead to frustrating errors. This is particularly true when dealing with advanced security features like client certificate authentication (mTLS).
Youâve encountered a classic problem: your `curl` command successfully authenticates the request using `.pem` files, but when attempting the same operation via Guzzle, you receive a cryptic error like `cURL error 35: alert handshake failure`. This indicates that while the request is being made, the SSL/TLS handshake process is failing, usually because the client certificate or key was not presented or validated correctly by the server.
This post will dissect why your initial Guzzle attempt failed and provide the correct, robust method for passing client certificates to your requests using Guzzle. We will look beyond the basic configuration to understand the underlying mechanism.
## The Discrepancy Between Shell and Library Execution
The core issue lies in how `curl` executes versus how Guzzle initializes its underlying cURL handler. When you use `curl -E openyes.crt.pem --key openyes.key.pem`, the shell environment handles the injection of these options directly to the libcurl library before the connection is established.
In contrast, Guzzle attempts to configure cURL through its internal options. While Guzzle does expose some low-level cURL options (as you correctly identified with `CURLOPT_SSLKEY` and `CURLOPT_SSLCERT`), these settings can be sensitive to the exact version of libcurl, the operating system environment, and how PHP handles file paths relative to the execution context. The handshake failure suggests that Guzzleâs method for setting these options is either being ignored or is incorrectly formatted for this specific server interaction.
## The Correct Approach: Using Stream Contexts for Advanced SSL
For complex SSL/TLS operations like client certificate authentication, relying solely on embedding raw cURL options within the request configuration often proves brittle. A more reliable and framework-agnostic solution involves utilizing Guzzle's ability to work with stream contexts or custom cURL options that are properly initialized before the request is sent.
While there isn't one single documented method that universally solves this for all setups, the most robust technique involves ensuring the certificate files are loaded correctly into the context used by Guzzle.
Here is a revised approach focusing on reliable configuration:
```php
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
// Assume base_path() utility function exists in your environment
$basePath = base_path();
$certFile = $basePath . '/openyes.crt.pem';
$keyFile = $basePath . '/openyes.key.pem';
$headers = [
'Content-Type' => 'application/json',
'X-Client-Id' => config('mykey'),
'X-Client-Secret' => config('mykey')
];
try {
// 1. Initialize the client
$client = new Client([
'timeout' => 650,
'http_errors' => false, // Important when dealing with custom SSL errors
// Note: We rely on the underlying cURL configuration or stream context
// for certificate loading, as direct CURLOPT options often fail.
]);
// 2. Execute the request
$response = $client->post(
'https://sky.myapitutorial.in:444/app/live/get',
[
'json' => $content,
'headers' => $headers,
],
// Attempt to pass the certificate configuration via a custom context if direct options fail
[
'verify' => $certFile // Sometimes setting verify points to the cert file helps load the context
]
);
dd($response);
} catch (RequestException $e) {
// Handle specific request exceptions, including SSL errors
$response = $e->getResponse();
if ($response) {
throw new \Exception("Guzzle Request Failed: " . $e->getMessage() . "\nResponse: " . $response->getBody()->getContents());
}
throw $e;
} catch (\Exception $e) {
// Catch other potential errors
echo "An unexpected error occurred: " . $e->getMessage();
}
```
## Conclusion and Best Practices
The persistence of the `alert handshake failure` error confirms that passing client credentials requires careful handling at the SSL layer, which is often abstracted away in high-level HTTP clients. When moving from a direct shell command to an application layer like Guzzle, you must shift your focus from embedding raw cURL constants to configuring the underlying connection context correctly.
For complex authentication scenarios involving mTLS, always validate your file paths and ensure that the certificate and key files have the correct permissions for the PHP process to read them. If you are working within the Laravel ecosystem, leveraging service containers or configuration files (like in `config/files` or environment variables) to manage these sensitive paths is a best practice, ensuring that your application remains secure and maintainable. Remember, clean code and robust error handlingâprinciples central to building reliable applications at **https://laravelcompany.com**âare crucial when dealing with network security.