diff --git a/UPGRADE-6.3.md b/UPGRADE-6.3.md index dcc98bf2cfc3..883fef0ba544 100644 --- a/UPGRADE-6.3.md +++ b/UPGRADE-6.3.md @@ -68,6 +68,8 @@ FrameworkBundle HttpClient ---------- + * The minimum TLS version now defaults to v1.2; use the `crypto_method` + option if you need to connect to servers that don't support it * The default user agents have been renamed from `Symfony HttpClient/Amp`, `Symfony HttpClient/Curl` and `Symfony HttpClient/Native` to `Symfony HttpClient (Amp)`, `Symfony HttpClient (Curl)` and `Symfony HttpClient (Native)` respectively to comply with the RFC 9110 specification diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index d668d435a42e..0dd13d245dce 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1817,7 +1817,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('A network interface name, IP address, a host name or a UNIX socket to bind to.') ->end() ->booleanNode('verify_peer') - ->info('Indicates if the peer should be verified in an SSL/TLS context.') + ->info('Indicates if the peer should be verified in a TLS context.') ->end() ->booleanNode('verify_host') ->info('Indicates if the host should exist as a certificate common name.') @@ -1838,7 +1838,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('The passphrase used to encrypt the "local_pk" file.') ->end() ->scalarNode('ciphers') - ->info('A list of SSL/TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)') + ->info('A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)') ->end() ->arrayNode('peer_fingerprint') ->info('Associative array: hashing algorithm => hash(es).') @@ -1849,6 +1849,9 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->variableNode('md5')->end() ->end() ->end() + ->scalarNode('crypto_method') + ->info('The minimum version of TLS to accept; must be one of STREAM_CRYPTO_METHOD_TLSv*_CLIENT constants.') + ->end() ->arrayNode('extra') ->info('Extra options for specific HTTP client') ->normalizeKeys(false) @@ -1965,7 +1968,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('A network interface name, IP address, a host name or a UNIX socket to bind to.') ->end() ->booleanNode('verify_peer') - ->info('Indicates if the peer should be verified in an SSL/TLS context.') + ->info('Indicates if the peer should be verified in a TLS context.') ->end() ->booleanNode('verify_host') ->info('Indicates if the host should exist as a certificate common name.') @@ -1986,7 +1989,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode, callable $e ->info('The passphrase used to encrypt the "local_pk" file.') ->end() ->scalarNode('ciphers') - ->info('A list of SSL/TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)') + ->info('A list of TLS ciphers separated by colons, commas or spaces (e.g. "RC3-SHA:TLS13-AES-128-GCM-SHA256"...)') ->end() ->arrayNode('peer_fingerprint') ->info('Associative array: hashing algorithm => hash(es).') diff --git a/src/Symfony/Component/HttpClient/AmpHttpClient.php b/src/Symfony/Component/HttpClient/AmpHttpClient.php index 94d8fa414ee4..26b1977314de 100644 --- a/src/Symfony/Component/HttpClient/AmpHttpClient.php +++ b/src/Symfony/Component/HttpClient/AmpHttpClient.php @@ -47,6 +47,10 @@ final class AmpHttpClient implements HttpClientInterface, LoggerAwareInterface, use HttpClientTrait; use LoggerAwareTrait; + public const OPTIONS_DEFAULTS = HttpClientInterface::OPTIONS_DEFAULTS + [ + 'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + ]; + private array $defaultOptions = self::OPTIONS_DEFAULTS; private static array $emptyDefaults = self::OPTIONS_DEFAULTS; private AmpClientState $multi; diff --git a/src/Symfony/Component/HttpClient/CHANGELOG.md b/src/Symfony/Component/HttpClient/CHANGELOG.md index 7cd69191c541..ae2111ac71f7 100644 --- a/src/Symfony/Component/HttpClient/CHANGELOG.md +++ b/src/Symfony/Component/HttpClient/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 6.3 --- + * Add option `crypto_method` to set the minimum TLS version and make it default to v1.2 * Add `UriTemplateHttpClient` to use URI templates as specified in the RFC 6570 * Add `ServerSentEvent::getArrayData()` to get the Server-Sent Event's data decoded as an array when it's a JSON payload * Allow array of urls as `base_uri` option value in `RetryableHttpClient` to retry on a new url each time diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index 5c19ed0bf1e6..ccb2a5707e87 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -36,6 +36,10 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, { use HttpClientTrait; + public const OPTIONS_DEFAULTS = HttpClientInterface::OPTIONS_DEFAULTS + [ + 'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + ]; + private array $defaultOptions = self::OPTIONS_DEFAULTS + [ 'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the // password as the second one; or string like username:password - enabling NTLM auth @@ -116,6 +120,12 @@ public function request(string $method, string $url, array $options = []): Respo \CURLOPT_SSLKEY => $options['local_pk'], \CURLOPT_KEYPASSWD => $options['passphrase'], \CURLOPT_CERTINFO => $options['capture_peer_cert_chain'], + \CURLOPT_SSLVERSION => match ($options['crypto_method']) { + \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT => \CURL_SSLVERSION_TLSv1_3, + \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT => \CURL_SSLVERSION_TLSv1_2, + \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT => \CURL_SSLVERSION_TLSv1_1, + \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT => \CURL_SSLVERSION_TLSv1_0, + }, ]; if (1.0 === (float) $options['http_version']) { diff --git a/src/Symfony/Component/HttpClient/HttpClientTrait.php b/src/Symfony/Component/HttpClient/HttpClientTrait.php index c767ca81aac0..48782153b407 100644 --- a/src/Symfony/Component/HttpClient/HttpClientTrait.php +++ b/src/Symfony/Component/HttpClient/HttpClientTrait.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpClient\Response\StreamableInterface; use Symfony\Component\HttpClient\Response\StreamWrapper; use Symfony\Component\Mime\MimeTypes; +use Symfony\Contracts\HttpClient\HttpClientInterface; /** * Provides the common logic from writing HttpClientInterface implementations. @@ -116,6 +117,15 @@ private static function prepareRequest(?string $method, ?string $url, array $opt $options['peer_fingerprint'] = self::normalizePeerFingerprint($options['peer_fingerprint']); } + if (isset($options['crypto_method']) && !\in_array($options['crypto_method'], [ + \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT, + \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT, + \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT, + ], true)) { + throw new InvalidArgumentException('Option "crypto_method" must be one of "STREAM_CRYPTO_METHOD_TLSv1_*_CLIENT".'); + } + // Validate on_progress if (isset($options['on_progress']) && !\is_callable($onProgress = $options['on_progress'])) { throw new InvalidArgumentException(sprintf('Option "on_progress" must be callable, "%s" given.', get_debug_type($onProgress))); diff --git a/src/Symfony/Component/HttpClient/Internal/AmpClientState.php b/src/Symfony/Component/HttpClient/Internal/AmpClientState.php index a83176629b9d..539bde252a42 100644 --- a/src/Symfony/Component/HttpClient/Internal/AmpClientState.php +++ b/src/Symfony/Component/HttpClient/Internal/AmpClientState.php @@ -126,6 +126,7 @@ private function getClient(array $options): array 'ciphers' => $options['ciphers'], 'capture_peer_cert_chain' => $options['capture_peer_cert_chain'] || $options['peer_fingerprint'], 'proxy' => $options['proxy'], + 'crypto_method' => $options['crypto_method'], ]; $key = hash('xxh128', serialize($options)); @@ -141,6 +142,7 @@ private function getClient(array $options): array $options['local_cert'] && $context = $context->withCertificate(new Certificate($options['local_cert'], $options['local_pk'])); $options['ciphers'] && $context = $context->withCiphers($options['ciphers']); $options['capture_peer_cert_chain'] && $context = $context->withPeerCapturing(); + $options['crypto_method'] && $context = $context->withMinimumVersion($options['crypto_method']); $connector = $handleConnector = new class() implements Connector { public $connector; diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index e4a2b0c28c67..c8f382efd7b6 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -36,6 +36,10 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac use HttpClientTrait; use LoggerAwareTrait; + public const OPTIONS_DEFAULTS = HttpClientInterface::OPTIONS_DEFAULTS + [ + 'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + ]; + private array $defaultOptions = self::OPTIONS_DEFAULTS; private static array $emptyDefaults = self::OPTIONS_DEFAULTS; @@ -198,6 +202,12 @@ public function request(string $method, string $url, array $options = []): Respo $options['timeout'] = min($options['max_duration'], $options['timeout']); } + switch ($cryptoMethod = $options['crypto_method']) { + case \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT: $cryptoMethod |= \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + case \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT: $cryptoMethod |= \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; + case \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT: $cryptoMethod |= \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT; + } + $context = [ 'http' => [ 'protocol_version' => min($options['http_version'] ?: '1.1', '1.1'), @@ -224,6 +234,7 @@ public function request(string $method, string $url, array $options = []): Respo 'allow_self_signed' => (bool) $options['peer_fingerprint'], 'SNI_enabled' => true, 'disable_compression' => true, + 'crypto_method' => $cryptoMethod, ], static fn ($v) => null !== $v), 'socket' => [ 'bindto' => $options['bindto'], diff --git a/src/Symfony/Contracts/CHANGELOG.md b/src/Symfony/Contracts/CHANGELOG.md index d8dd36863baf..d944ba18707b 100644 --- a/src/Symfony/Contracts/CHANGELOG.md +++ b/src/Symfony/Contracts/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +3.3 +--- + + * Add option `crypto_method` to `HttpClientInterface` to define the minimum TLS version to accept + 3.2 --- diff --git a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php index b148b19ac819..59636258ff6e 100644 --- a/src/Symfony/Contracts/HttpClient/HttpClientInterface.php +++ b/src/Symfony/Contracts/HttpClient/HttpClientInterface.php @@ -66,6 +66,7 @@ interface HttpClientInterface 'ciphers' => null, 'peer_fingerprint' => null, 'capture_peer_cert_chain' => false, + 'crypto_method' => \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, // STREAM_CRYPTO_METHOD_TLSv*_CLIENT - minimum TLS version 'extra' => [], // array - additional options that can be ignored if unsupported, unlike regular options ];