diff --git a/docs/request-options.rst b/docs/request-options.rst index f45665505..e06f3b953 100644 --- a/docs/request-options.rst +++ b/docs/request-options.rst @@ -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 @@ -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 @@ -1053,6 +1109,28 @@ SSL certificates can be found on the `cURL website `_. +.. _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 diff --git a/src/Handler/CurlFactory.php b/src/Handler/CurlFactory.php index 16a942232..8973c3358 100644 --- a/src/Handler/CurlFactory.php +++ b/src/Handler/CurlFactory.php @@ -382,6 +382,15 @@ private function applyHandlerOptions(EasyHandle $easy, array &$conf): void } } + 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']; + } + if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { $accept = $easy->request->getHeaderLine('Accept-Encoding'); if ($accept) { @@ -498,6 +507,24 @@ private function applyHandlerOptions(EasyHandle $easy, array &$conf): void $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; + } + if (isset($options['ssl_key'])) { if (\is_array($options['ssl_key'])) { if (\count($options['ssl_key']) === 2) { @@ -515,6 +542,24 @@ private function applyHandlerOptions(EasyHandle $easy, array &$conf): void $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; + } + if (isset($options['progress'])) { $progress = $options['progress']; if (!\is_callable($progress)) { diff --git a/src/RequestOptions.php b/src/RequestOptions.php index a38768c0c..173dadfdf 100644 --- a/src/RequestOptions.php +++ b/src/RequestOptions.php @@ -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 @@ -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. @@ -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). diff --git a/tests/Handler/CurlFactoryTest.php b/tests/Handler/CurlFactoryTest.php index af88b0966..edbc7e3ce 100644 --- a/tests/Handler/CurlFactoryTest.php +++ b/tests/Handler/CurlFactoryTest.php @@ -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); @@ -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); @@ -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);