From a8154fcb9410c1f78414e5d0772fc69e46d5ce39 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sat, 13 May 2023 17:28:03 +0200 Subject: [PATCH] Add support for setting crypto method in a unified way (#3132) --- docs/request-options.rst | 22 ++++++++++++++++ src/Handler/CurlFactory.php | 26 +++++++++++++++++++ src/Handler/StreamHandler.php | 19 ++++++++++++++ src/RequestOptions.php | 12 +++++++++ tests/Handler/CurlFactoryTest.php | 40 +++++++++++++++++++++++++++++ tests/Handler/StreamHandlerTest.php | 39 ++++++++++++++++++++++++++++ 6 files changed, 158 insertions(+) diff --git a/docs/request-options.rst b/docs/request-options.rst index a4407030d..61ebd6112 100644 --- a/docs/request-options.rst +++ b/docs/request-options.rst @@ -299,6 +299,28 @@ connect_timeout handler. +.. _crypto_method-option: + +crypto_method +--------------- + +:Summary: A value describing the minimum TLS protocol version to use. +:Types: int +:Default: None +:Constant: ``GuzzleHttp\RequestOptions::CRYPTO_METHOD`` + +.. code-block:: php + + $client->request('GET', '/foo', ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT]); + +.. note:: + + This setting must be set to one of the ``STREAM_CRYPTO_METHOD_TLS*_CLIENT`` + constants. PHP 7.4 or higher is required in order to use TLS 1.3, and cURL + 7.34.0 or higher is required in order to specify a crypto method, with cURL + 7.52.0 or higher being required to use TLS 1.3. + + .. _debug-option: debug diff --git a/src/Handler/CurlFactory.php b/src/Handler/CurlFactory.php index e8f5fe8c6..dc8f97c67 100644 --- a/src/Handler/CurlFactory.php +++ b/src/Handler/CurlFactory.php @@ -452,6 +452,32 @@ private function applyHandlerOptions(EasyHandle $easy, array &$conf): void } } + if (isset($options['crypto_method'])) { + if (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_0')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.0 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0; + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_1')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.1 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1; + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_2')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; + } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { + if (!defined('CURL_SSLVERSION_TLSv1_3')) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; + } else { + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); + } + } + if (isset($options['cert'])) { $cert = $options['cert']; if (\is_array($cert)) { diff --git a/src/Handler/StreamHandler.php b/src/Handler/StreamHandler.php index 543f825a2..7bd4b1176 100644 --- a/src/Handler/StreamHandler.php +++ b/src/Handler/StreamHandler.php @@ -472,6 +472,25 @@ private function add_timeout(RequestInterface $request, array &$options, $value, } } + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params): void + { + if ( + $value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT + || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + || (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) + ) { + $options['http']['crypto_method'] = $value; + + return; + } + + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); + } + /** * @param mixed $value as passed via Request transfer options. */ diff --git a/src/RequestOptions.php b/src/RequestOptions.php index 20b31bc20..5679f32f1 100644 --- a/src/RequestOptions.php +++ b/src/RequestOptions.php @@ -74,6 +74,18 @@ final class RequestOptions */ public const CONNECT_TIMEOUT = 'connect_timeout'; + /** + * crypto_method: (int) A value describing the minimum TLS protocol + * version to use. + * + * This setting must be set to one of the + * ``STREAM_CRYPTO_METHOD_TLS*_CLIENT`` constants. PHP 7.4 or higher is + * required in order to use TLS 1.3, and cURL 7.34.0 or higher is required + * in order to specify a crypto method, with cURL 7.52.0 or higher being + * required to use TLS 1.3. + */ + public const CRYPTO_METHOD = 'crypto_method'; + /** * debug: (bool|resource) Set to true or set to a PHP stream returned by * fopen() enable debug output with the HTTP handler used to send a diff --git a/tests/Handler/CurlFactoryTest.php b/tests/Handler/CurlFactoryTest.php index 254d7c188..166508615 100644 --- a/tests/Handler/CurlFactoryTest.php +++ b/tests/Handler/CurlFactoryTest.php @@ -221,6 +221,46 @@ public function testUsesProxy() self::assertSame('hi', (string) $response->getBody()); } + public function testValidatesCryptoMethodInvalidMethod() + { + $f = new Handler\CurlFactory(3); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid crypto_method request option: unknown version provided'); + $f->create(new Psr7\Request('GET', Server::$url), ['crypto_method' => 123]); + } + + public function testAddsCryptoMethodTls10() + { + $f = new Handler\CurlFactory(3); + $f->create(new Psr7\Request('GET', Server::$url), ['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT]); + self::assertEquals(\CURL_SSLVERSION_TLSv1_0, $_SERVER['_curl'][\CURLOPT_SSLVERSION]); + } + + public function testAddsCryptoMethodTls11() + { + $f = new Handler\CurlFactory(3); + $f->create(new Psr7\Request('GET', Server::$url), ['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT]); + self::assertEquals(\CURL_SSLVERSION_TLSv1_1, $_SERVER['_curl'][\CURLOPT_SSLVERSION]); + } + + public function testAddsCryptoMethodTls12() + { + $f = new Handler\CurlFactory(3); + $f->create(new Psr7\Request('GET', Server::$url), ['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT]); + self::assertEquals(\CURL_SSLVERSION_TLSv1_2, $_SERVER['_curl'][\CURLOPT_SSLVERSION]); + } + + /** + * @requires PHP >= 7.4 + */ + public function testAddsCryptoMethodTls13() + { + $f = new Handler\CurlFactory(3); + $f->create(new Psr7\Request('GET', Server::$url), ['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]); + self::assertEquals(\CURL_SSLVERSION_TLSv1_3, $_SERVER['_curl'][\CURLOPT_SSLVERSION]); + } + public function testValidatesSslKey() { $f = new Handler\CurlFactory(3); diff --git a/tests/Handler/StreamHandlerTest.php b/tests/Handler/StreamHandlerTest.php index 41f293ed1..135308391 100644 --- a/tests/Handler/StreamHandlerTest.php +++ b/tests/Handler/StreamHandlerTest.php @@ -379,6 +379,45 @@ public function testEnsuresVerifyOptionIsValid() $this->getSendResult(['verify' => 10]); } + public function testEnsuresCryptoMethodOptionIsValid() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid crypto_method request option: unknown version provided'); + + $this->getSendResult(['crypto_method' => 123]); + } + + public function testSetsCryptoMethodTls10() + { + $res = $this->getSendResult(['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT]); + $opts = \stream_context_get_options($res->getBody()->detach()); + self::assertSame(\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT, $opts['http']['crypto_method']); + } + + public function testSetsCryptoMethodTls11() + { + $res = $this->getSendResult(['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT]); + $opts = \stream_context_get_options($res->getBody()->detach()); + self::assertSame(\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT, $opts['http']['crypto_method']); + } + + public function testSetsCryptoMethodTls12() + { + $res = $this->getSendResult(['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT]); + $opts = \stream_context_get_options($res->getBody()->detach()); + self::assertSame(\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, $opts['http']['crypto_method']); + } + + /** + * @requires PHP >=7.4 + */ + public function testSetsCryptoMethodTls13() + { + $res = $this->getSendResult(['crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT]); + $opts = \stream_context_get_options($res->getBody()->detach()); + self::assertSame(\STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, $opts['http']['crypto_method']); + } + public function testCanSetPasswordWhenSettingCert() { $path = __FILE__;