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

Add options to allow passing certificates by string #3202

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 78 additions & 0 deletions docs/request-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,39 @@ cert
$client->request('GET', '/', ['cert' => ['/path/server.pem', 'password']]);


.. _cert_blob-option:

cert-blob
----

:Summary: Set to a string containing a formatted client side certificate.
If a password is required, then set to an array containing the PEM certificate
and the password.
If the certificate format is 'DER' or 'P12' the type must be specified.
:Types:
- string
- array
:Default: None
:Constant: ``GuzzleHttp\RequestOptions::CERT_BLOB``

.. code-block:: php

$client->request('GET', '/', [
'cert_blob' => [
'cert' => 'certificate',
'password' => 'password',
'type' => 'P12',
],
]);

.. note::

``cert_blob`` is implemented by HTTP handlers. This is currently only
supported by the cURL handler, but might be supported by other third-part
handlers.
The option is available in PHP >= 8.1


.. _cookies-option:

cookies
Expand Down Expand Up @@ -976,6 +1009,29 @@ ssl_key
handlers.


.. _ssl_key_blob-option:

ssl_key_blob
-------

:Summary: Specify a string containing a private SSL key in PEM format.
If a password is required, then set to an array containing the SSL key
in the first array element followed by the password required for the
certificate in the second element.
:Types:
- string
- array
:Default: None
:Constant: ``GuzzleHttp\RequestOptions::SSL_KEY_BLOB``

.. note::

``ssl_key_blob`` is implemented by HTTP handlers. This is currently only
supported by the cURL handler, but might be supported by other third-part
handlers.
The option is available in PHP >= 8.1


.. _stream-option:

stream
Expand Down Expand Up @@ -1053,6 +1109,28 @@ SSL certificates can be found on the
`cURL website <http://curl.haxx.se/docs/sslcerts.html>`_.


.. _verify_blob-option:

verify_blob
------

:Summary: Specify the CA bundle to use for SSL certificate verification. When this
option is used certificate verification is enforced.
:Types: string
:Constant: ``GuzzleHttp\RequestOptions::VERIFY_BLOB``

.. code-block:: php

$client->request('GET', '/', ['verify_blob' => 'certificates']);

.. note::

``verify_blob`` is implemented by HTTP handlers. This is currently only
supported by the cURL handler, but might be supported by other third-part
handlers.
The option is available in PHP >= 8.2


.. _timeout-option:

timeout
Expand Down
45 changes: 45 additions & 0 deletions src/Handler/CurlFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,15 @@
}
}

if (isset($options['verify_blob'])) {
if (\version_compare(PHP_VERSION, '8.2.0', '<')) {
throw new \InvalidArgumentException('verify blob option is available in PHP >= 8.2');
}
$conf[\CURLOPT_SSL_VERIFYHOST] = 2;
$conf[\CURLOPT_SSL_VERIFYPEER] = true;
$conf[\CURLOPT_CAINFO_BLOB] = $options['verify_blob'];

Check failure on line 391 in src/Handler/CurlFactory.php

View workflow job for this annotation

GitHub Actions / PHPStan

Constant CURLOPT_CAINFO_BLOB not found.

Check failure on line 391 in src/Handler/CurlFactory.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedConstant

src/Handler/CurlFactory.php:391:19: UndefinedConstant: Const CURLOPT_CAINFO_BLOB is not defined (see https://psalm.dev/020)
}

if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) {
$accept = $easy->request->getHeaderLine('Accept-Encoding');
if ($accept) {
Expand Down Expand Up @@ -498,6 +507,24 @@
$conf[\CURLOPT_SSLCERT] = $cert;
}

if (isset($options['cert_blob'])) {
if (\version_compare(PHP_VERSION, '8.1.0', '<')) {
throw new \InvalidArgumentException('cert blob option is available in PHP >= 8.1');
}
$cert = $options['cert_blob'];
if (\is_array($cert)) {
if (!empty($cert['password'])) {
$conf[\CURLOPT_SSLCERTPASSWD] = $cert['password'];
}
if (!empty($cert['type'])) {
$conf[\CURLOPT_SSLCERTTYPE] = strtoupper($cert['type']);
}
$cert = $cert['cert'];
}

$conf[\CURLOPT_SSLCERT_BLOB] = $cert;

Check failure on line 525 in src/Handler/CurlFactory.php

View workflow job for this annotation

GitHub Actions / PHPStan

Constant CURLOPT_SSLCERT_BLOB not found.

Check failure on line 525 in src/Handler/CurlFactory.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedConstant

src/Handler/CurlFactory.php:525:19: UndefinedConstant: Const CURLOPT_SSLCERT_BLOB is not defined (see https://psalm.dev/020)
}

if (isset($options['ssl_key'])) {
if (\is_array($options['ssl_key'])) {
if (\count($options['ssl_key']) === 2) {
Expand All @@ -515,6 +542,24 @@
$conf[\CURLOPT_SSLKEY] = $sslKey;
}

if (isset($options['ssl_key_blob'])) {
if (\version_compare(PHP_VERSION, '8.1.0', '<')) {
throw new \InvalidArgumentException('ssl key blob option is available in PHP >= 8.1');
}

if (\is_array($options['ssl_key_blob'])) {
if (\count($options['ssl_key_blob']) === 2) {
[$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key_blob'];
} else {
[$sslKey] = $options['ssl_key_blob'];
}
}

$sslKey = $sslKey ?? $options['ssl_key_blob'];

$conf[\CURLOPT_SSLKEY_BLOB] = $sslKey;

Check failure on line 560 in src/Handler/CurlFactory.php

View workflow job for this annotation

GitHub Actions / PHPStan

Constant CURLOPT_SSLKEY_BLOB not found.

Check failure on line 560 in src/Handler/CurlFactory.php

View workflow job for this annotation

GitHub Actions / Psalm

UndefinedConstant

src/Handler/CurlFactory.php:560:19: UndefinedConstant: Const CURLOPT_SSLKEY_BLOB is not defined (see https://psalm.dev/020)
}

if (isset($options['progress'])) {
$progress = $options['progress'];
if (!\is_callable($progress)) {
Expand Down
22 changes: 22 additions & 0 deletions src/RequestOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ final class RequestOptions
*/
public const CERT = 'cert';

/**
* cert_blob: (string|array) Set to a string containing a
* SSL client side certificate. If a password is required, then set
* cert to an array.
* If the certificate format is 'DER' or 'P12' the type must be specified.
*/
public const CERT_BLOB = 'cert_blob';

/**
* cookies: (bool|GuzzleHttp\Cookie\CookieJarInterface, default=false)
* Specifies whether or not cookies are used in a request or what cookie
Expand Down Expand Up @@ -234,6 +242,14 @@ final class RequestOptions
*/
public const SSL_KEY = 'ssl_key';

/**
* ssl_key_blob: (array|string) Specify a string containing a private
* SSL key in PEM format. If a password is required, then set to an array
* containing the SSL key in the first array element followed
* by the password required for the certificate in the second element.
*/
public const SSL_KEY_BLOB = 'ssl_key_blob';

/**
* stream: Set to true to attempt to stream a response rather than
* download it all up-front.
Expand All @@ -250,6 +266,12 @@ final class RequestOptions
*/
public const VERIFY = 'verify';

/**
* verify_blob: (string) Specify the CA bundle to use for SSL certificate
* verification. When this option is used certificate verification is enforced.
*/
public const VERIFY_BLOB = 'verify_blob';

/**
* timeout: (float, default=0) Float describing the timeout of the
* request in seconds. Use 0 to wait indefinitely (the default behavior).
Expand Down
102 changes: 102 additions & 0 deletions tests/Handler/CurlFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,18 @@ public function testCanDisableVerify()
self::assertFalse($_SERVER['_curl'][\CURLOPT_SSL_VERIFYPEER]);
}

/**
* @requires PHP >= 8.4
*/
public function testCanSetVerifyBlob()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', 'http://foo.com'), ['verify' => __FILE__]);
self::assertEquals(__FILE__, $_SERVER['_curl'][\CURLOPT_CAINFO]);
self::assertEquals(2, $_SERVER['_curl'][\CURLOPT_SSL_VERIFYHOST]);
self::assertTrue($_SERVER['_curl'][\CURLOPT_SSL_VERIFYPEER]);
}

public function testAddsProxy()
{
$f = new Handler\CurlFactory(3);
Expand Down Expand Up @@ -293,6 +305,38 @@ public function testAddsSslKeyWhenUsingArraySyntaxButNoPassword()
self::assertEquals(__FILE__, $_SERVER['_curl'][\CURLOPT_SSLKEY]);
}

/**
* @requires PHP >= 8.1
*/
public function testAddsSslKeyBlob()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', Server::$url), ['ssl_key_blob' => 'certificate']);
self::assertEquals('certificate', $_SERVER['_curl'][\CURLOPT_SSLKEY_BLOB]);
}

/**
* @requires PHP >= 8.1
*/
public function testAddsSslKeyBlobWithPassword()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', Server::$url), ['ssl_key_blob' => ['certificate', 'test']]);
self::assertEquals('certificate', $_SERVER['_curl'][\CURLOPT_SSLKEY_BLOB]);
self::assertEquals('test', $_SERVER['_curl'][\CURLOPT_SSLKEYPASSWD]);
}

/**
* @requires PHP >= 8.1
*/
public function testAddsSslKeyBlobWhenUsingArraySyntaxButNoPassword()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', Server::$url), ['ssl_key_blob' => [__FILE__]]);

self::assertEquals(__FILE__, $_SERVER['_curl'][\CURLOPT_SSLKEY_BLOB]);
}

public function testValidatesCert()
{
$f = new Handler\CurlFactory(3);
Expand Down Expand Up @@ -345,6 +389,64 @@ public function testAddsP12Cert()
}
}

/**
* @requires PHP >= 8.1
*/
public function testAddsCertBlob()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', Server::$url), ['cert_blob' => 'certificate']);
self::assertEquals('certificate', $_SERVER['_curl'][\CURLOPT_SSLCERT_BLOB]);
}

/**
* @requires PHP >= 8.1
*/
public function testAddsCertBlobWithPassword()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', Server::$url), [
'cert_blob' => [
'cert' => 'certificate',
'password' => 'test',
],
]);
self::assertEquals('certificate', $_SERVER['_curl'][\CURLOPT_SSLCERT_BLOB]);
self::assertEquals('test', $_SERVER['_curl'][\CURLOPT_SSLCERTPASSWD]);
}

/**
* @requires PHP >= 8.1
*/
public function testAddsDerCertBlob()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', Server::$url), [
'cert_blob' => [
'cert' => 'certificate',
'type' => 'der',
],
]);
self::assertArrayHasKey(\CURLOPT_SSLCERTTYPE, $_SERVER['_curl']);
self::assertEquals('DER', $_SERVER['_curl'][\CURLOPT_SSLCERTTYPE]);
}

/**
* @requires PHP >= 8.1
*/
public function testAddsP12CertBlob()
{
$f = new Handler\CurlFactory(3);
$f->create(new Psr7\Request('GET', Server::$url), [
'cert_blob' => [
'cert' => 'certificate',
'type' => 'P12',
],
]);
self::assertArrayHasKey(\CURLOPT_SSLCERTTYPE, $_SERVER['_curl']);
self::assertEquals('P12', $_SERVER['_curl'][\CURLOPT_SSLCERTTYPE]);
}

public function testValidatesProgress()
{
$f = new Handler\CurlFactory(3);
Expand Down