This is an old revision of the document!
Improved TLS Defaults
- Version: 0.10
- Date: 2014-01-28
- Author: Daniel Lowrey, rdlowrey@php.net
- Status: Vote
- First Published at: http://wiki.php.net/rfc/improved-tls-defaults
- Minor Revision (v0.1 → v0.2): 2014-01-28
- Major Revision (v0.2 → v0.3): 2014-01-29
- Minor Revision (v0.3 → v0.4): 2014-01-30
- Minor Revision (v0.4 → v0.5): 2014-01-30
- Major Revision (v0.5 → v0.6): 2014-02-01
- Minor Revision (v0.6 → v0.7): 2014-02-02
- Minor Revision (v0.7 → v0.8): 2014-02-03
- Minor Revision (v0.8 → v0.9): 2014-02-06
- Minor Revision (v0.9 → v0.10): 2014-02-11
Introduction
If you're short on time: TL;DR Definitive Progress
PHP stream encryption uses several potentially insecure default settings. This RFC explores the problematic nature of the current settings and recommends actionable improvements. The proposed changes increase the security of encrypted stream transmissions at the same time they eliminate the need for user knowledge of the underlying protocols.
This proposal complements the previously accepted TLS Peer Verification RFC which is insufficient as a standalone measure to address potential TLS pitfalls. The backward compatibility costs of the recommendations found here are congruent with those found in the previous RFC. Note that BC breakage is not seen at the userland API level (where all changes are compatible with existing code). Instead, disruptions can occur in the form of transfer failures which were previously allowed to proceed in an insecure manner. As all streams are inherently vulnerable to failure at any time, userland code should have existing error handling mechanisms in place to address any failures arising from the proposed security enhancements.
In short, this proposal exists because peer verification is only half of the secure stream encryption equation. The verification of peer certificates affords little protection if the implementation subsequently engages in other insecure practices. This proposal addresses the remaining side of the encrypted transfer equation to transparently maximize data security.
Proposed Change Summary
Default Ciphers
Currently all encrypted stream transports use the openssl DEFAULT
cipher list unless manually
specified by the user via a “ciphers”
SSL context option. This behavior exposes unwitting users
to the possibility that very weak ciphers will be negotiated for SSL/TLS sessions. The use of such
ciphers renders otherwise-legitimate encryption measures ineffective against sophisticated attacks.
Proposal
Change the default cipher list from DEFAULT
to the following:
$ openssl ciphers -v 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256: ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256: DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256: ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384: ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA: DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256: AES256-GCM-SHA384:AES128:AES256:HIGH:!SSLv2:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!RC4:!ADH'
This list is congruent with the Mozilla cipher recommendations excepting two additional exclusions:
!ADH
- Anonymous Diffie-Hellman ciphers disallowed as per RFC2246 Section A.5
!RC4
Example
<?php // New informational constant exposed to userland var_dump(OPENSSL_DEFAULT_STREAM_CIPHERS); /** * All encrypted streams use the new default cipher list automatically */ $html = file_get_contents('https://somesite.com'); /** * Users may override the default ciphers at any time by manually specifying * their own list via the "ciphers" SSL context option. */ $context = stream_context_create(['ssl' => [ 'ciphers' => 'HIGH:MEDIUM:LOW@SPEED' ]]); $html = file_get_contents('https://somesite.com', null, $context); ?>
TLS Compression
As of PHP 5.4.13 users may specify the “disable_compression”
SSL context option to mitigate the
CRIME attack vector. However, to benefit from this protection users must recognize the threat and
manually assign the relevant context option.
Proposal
- Disable TLS compression by default.
Logic
Users are generally advised to disable compression as a low-cost method for mitigating CRIME attacks. It's sensible to enable this protection by default as users wishing to re-enable compression at the TLS layer may do so by assigning a falsy value to the “disable_compression” SSL context option.
Example
Because this option will be enabled by default, users won't require any action to reap the benefits. Users wishing to disable this setting may do so via the stream context as shown here:
<?php // How to enable TLS-layer compression (not recommended!) $context = stream_context_create(['ssl' => [ 'disable_compression' => FALSE ]]); $uri = 'https://www.bankofamerica.com/'; $html = file_get_contents($uri, FALSE, $context); ?>
Honor Cipher Order
The BEAST TLS attack
vector was first publicized in 2011.
Mitigating this attack is relatively simple: servers have only to prioritize ciphers
that aren't susceptible to the attack. However, unless instructed otherwise, OpenSSL uses the client's
preferences when negotiating the cipher. To prevent nefarious (or naive) clients from prioritizing
susceptible ciphers servers should configure SSL sessions using the SSL_OP_CIPHER_SERVER_PREFERENCE
OpenSSL context option.
Proposal
- Add a new boolean
“honor_cipher_order”
ssl context option.
Logic
Exposing this capability to userland allows encrypted stream servers to transparently mitigate BEAST vulnerabilities and control cipher ordering preferences during negotiation.
Example
<?php $context = stream_context_create(['ssl' => [ "crypto_method" => STREAM_CRYPTO_METHOD_TLS_SERVER, "local_cert" => "/path/to/my/server.pem", "local_pk" => "/path/to/my/private.key", "honor_cipher_order" => TRUE ]]); $socketFlags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; $server = stream_socket_server('tls://127.0.0.1:443', $errno, $errstr, $socketFlags, $context); ?>
Expose Negotiated Values
Proposal
- Expose access to the negotiated protocol/cipher via a new boolean
“capture_session_meta”
context option
Logic
Users may wish to access information regarding the negotiated protocol and/or cipher for a given
encrypted session. The stream_get_meta_data()
wrapper_data abstraction is avoided here to prevent
conflicts with other wrappers (I'm looking at you, http
wrapper). The context option approach
also remains consistent with the existing capture_peer_cert
and capture_peer_cert_chain
boolean
context abstractions. When the new context option is truthy the ['ssl']['session_meta']
context
option is populated with an informational array as shown here:
Example
<?php $context = stream_context_create(['ssl' => [ 'capture_session_meta' => TRUE ]]); $html = file_get_contents('https://www.bankofamerica.com/', FALSE, $context); $meta = stream_context_get_options($ctx)['ssl']['session_meta']; var_dump($meta); /* Array ( [protocol] => TLSv1.2 [cipher_name] => ECDHE-RSA-AES128-GCM-SHA256 [cipher_bits] => 128 [cipher_version] => TLSv1/SSLv3 ) */ ?>
Forward Secrecy for Servers
Proposal
Encrypted client streams already support forward secrecy as this functionality is largely implemented server-side. However, the patch proposed here makes it possible for encrypted PHP stream servers to also achieve (perfect) forward secrecy when negotiating cipher suites that utilize ephemeral key agreements.
New Context Options
The following new context options are added to allow customization of the relevant functionality and are only applicable for encrypted servers:
“ecdh_curve”
Servers may specify which curve to use with ECDH ciphers. If not specified prime256v1
will be used.
The following command will display the available curves in your openssl build:
$ openssl ecparam -list_curves
“dh_param”
A path to a file containing parameters for Diffie–Hellman key exchange. Users may create such a file using the following command:
$ openssl dhparam -out /path/to/my/certs/dh-2048.pem 2048
Note that some clients have interoperability issues with keys larger than 2048 bits in size. Java clients in particular are known not to work with anything larger than 1024 bits.
“single_dh_use”
Always create a new key pair when using DH parameters (improves forward secrecy).
“single_ecdh_use”
Always create a new key pair in scenarios where ECDH cipher suites are negotiated (instead of the preferred ECDHE ciphers). This option improves forward secrecy.
Example
<?php $context = stream_context_create(['ssl' => [ "local_cert" => "/path/to/my/server.pem", "local_pk" => "/path/to/my/private.key", "disable_compression" => TRUE, "honor_cipher_order" => TRUE, "ecdh_curve" => "secp384r1", // defaults to "prime256v1" "dh_param" => "/path/to/dh2048.pem" "single_ecdh_use" => TRUE, "single_dh_use" => TRUE ]]); $socketFlags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; $server = stream_socket_server('tcp://127.0.0.1:443', $errno, $errstr, $socketFlags, $context); stream_set_blocking($server, FALSE); // stream_socket_enable_crypto() is used after client sockets are accepted // to enable crypto in a non-blocking way ... stream_socket_enable_crypto($client, $enable = TRUE, STREAM_CRYPTO_SERVER); ?>
Expose Default Cert Paths
Proposal
- Add new
openssl_get_cert_locations()
function to simplify troubleshooting CA cert location problems now that peer verification is enabled by default
Example
<?php var_dump(openssl_get_cert_locations()); /* array(8) { ["default_cert_file"]=> string(21) "/usr/lib/ssl/cert.pem" ["default_cert_file_env"]=> string(13) "SSL_CERT_FILE" ["default_cert_dir"]=> string(18) "/usr/lib/ssl/certs" ["default_cert_dir_env"]=> string(12) "SSL_CERT_DIR" ["default_private_dir"]=> string(20) "/usr/lib/ssl/private" ["default_default_cert_area"]=> string(12) "/usr/lib/ssl" ["ini_cafile"]=> string(0) "" ["ini_capath"]=> string(0) "" } */ ?>
Stream Wrapper Creep
The following stream encryption wrappers currently exist in userland:
ssl
sslv2
sslv3
tls
Meanwhile, 5.6 has added the following new wrappers:
tlsv1.1
tlsv1.2
Yeah, So?
The problem with this design should be obvious: it grows linearly as each new encryption protocol is standardized and unleashed on the world. Choosing the correct wrapper is already a daunting task for users unfamiliar with the various transport layer security protocols and this situation will only deteriorate as new protocols are continuously adopted.
Beyond the “creep” of new stream wrappers there also exists a consistency problem. Do all users
understand that the ssl
wrapper technically can negotiate any of the supported protocols? Do
they know that in contrast the tls
wrapper will only negotiate TLSv1 and not the newer TLS
iterations? Do they realize that the ssl
wrapper potentially exposes their transfers to the
broken/insecure SSLv2 and SSLv3 protocols? How can they tell PHP to use (for example) only TLSv1.1
or TLSv1.2?
This design is confusing and has aged poorly in a world where new protocols arrive periodically to address the shortcomings of previous iterations. Moreover, PHP is built on the foundation of hiding these kinds of minute details from the user. Developers shouldn't need a full understanding of the underlying transport layer security protocols to safely encrypt their transfers.
The goal must always be to make things “just work” in a secure manner without requiring user knowledge of the underlying machinations.
Source of the Problem
This existing discrete stream wrapper approach is necessary because it depends on value assignments to determine the encryption protocol instead of flags. This makes design choice makes it impossible to achieve fine-grained control over which protocols are used without fractaling out new constants for every conceivable combination of protocols. The “value” approach essentially locks users into one of two choices:
- Allow only one narrow protocol
- Allow ALL of the protocols, even if some do not provide the requisite level of security
While this paradigm negatively impacts client-side applications, its shortcomings are particularly
acute for stream_socket_server()
users who require fine-grained control over which protocols are
allowed in their servers. For example, a server may wish to allow only TLSv1.1 and TLSv1.2 to
maximize transmission safety. The existing paradigm makes this level of control impossible.
Proposal
- Internally re-value the existing
STREAM_CRYPTO_METHOD_*
constants to allow the assignment of crypto methods using bitwise flags instead of values. Users may specify any combination of these constants to control the allowed protocols for a given client or server stream. Meanwhile, the“crypto_method”
context option already included as part of 5.6 allows all code to specify exactly which methods are appropriate for a given operation. - Remove the new stream wrappers previously merged for inclusion in 5.6:
tlsv1.1
tlsv1.2
- Deprecate the following stream wrappers in PHP 5.6 with removal (tentatively) scheduled for PHP 6:
sslv2
sslv3
Logic
The main reason for reshuffling the stream wrappers is simplicity. Most users are unlikely to know the difference between the various protocols much less which they should use. It's not difficult to imagine a scenario in which a user reasons, “If SSL is safe, SSLv2 and SSLv3 must be REALLY safe. I should use those.” Such users are likely to have no idea what “tls” means and forego it completely despite its standing as the most secure option.
So the overarching goal in this change is two-fold:
- Keep users safe by default without preventing them from doing insecure things if they need to do so;
- Eliminate the confusion of having many different wrappers for encrypted streams. By deprecating the superfluous stream wrappers we simplify the ecosystem at the same time we're actually adding new functionality (via customized flag protocol specification).
Note again that the protocol-specific stream wrappers are not removed. Their use will trigger an
E_DEPRECATED
warning but they will continue to function as before.
Note also that the tls
stream wrapper will now negotiate the best available of the TLS1, TLSv1.1
and TLSv1.2 protocols. Because ext/openssl
did not support TLSv1.1 and TLSv1.2 prior to PHP 5.6
this wrapper was previously only able to negotiate TLSv1.
BC Implications of Proposed Stream Wrapper Changes
None.
The only implications for existing code are the E_DEPRECATED
messages for the sslv2
and
sslv3
stream wrappers. Note that only stream wrapper usages trigger deprecation
notices. This notice triggering has nothing to do with the underlying protocols; it only occurs when
using the wrappers made unnecessary by the introduction of protocol flags. Users are encouraged to
specify protocol flags in their stream contexts to avoid these deprecation notices.
Existing Constant Re-Valuing
The existing constants are internally re-valued as shown below to allow their use as bitwise flags. Because the existing code delineates between clients and servers the least significant bit is used to differentiate between the two stream types.
typedef enum { STREAM_CRYPTO_METHOD_SSLv2_CLIENT = (1 << 1 | 1), STREAM_CRYPTO_METHOD_SSLv3_CLIENT = (1 << 2 | 1), STREAM_CRYPTO_METHOD_SSLv23_CLIENT = ((1 << 1) | (1 << 2) | 1), /* SSLv2 or SSLv3 */ STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT = (1 << 3 | 1), /* New in 5.6 */ STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT = (1 << 4 | 1), /* New in 5.6 */ STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT = (1 << 5 | 1), /* New in 5.6 */ STREAM_CRYPTO_METHOD_TLS_CLIENT = ((1 << 3) | (1 << 4) | (1 << 5) | 1), /* Any TLS protocol */ STREAM_CRYPTO_CLIENT = ((1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | 1), /* Any protocol */ STREAM_CRYPTO_METHOD_SSLv2_SERVER = (1 << 1), STREAM_CRYPTO_METHOD_SSLv3_SERVER = (1 << 2), STREAM_CRYPTO_METHOD_SSLv23_SERVER = ((1 << 1) | (1 << 2)), /* SSLv2 or SSLv3 */ STREAM_CRYPTO_METHOD_TLSv1_0_SERVER = (1 << 3), /* New in 5.6 */ STREAM_CRYPTO_METHOD_TLSv1_1_SERVER = (1 << 4), /* New in 5.6 */ STREAM_CRYPTO_METHOD_TLSv1_2_SERVER = (1 << 5), /* New in 5.6 */ STREAM_CRYPTO_METHOD_TLS_SERVER = ((1 << 3) | (1 << 4) | (1 << 5)) /* Any TLS protocol */ STREAM_CRYPTO_SERVER = ((1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5)), /* Any protocol */ } php_stream_xport_crypt_method_t;
These internal enum values map directly to the existing userland constants of the same name. Astute
readers may notice that the SSLv23
constants do not carry the same meaning as previous versions
of the openssl extension. As far as the underlying OpenSSL library is concerned, SSLv23
translates to “every protocol you can possibly support (including TLS protocols).” This reuse of a
legacy naming convention is a source of constant confusion for users not versed in the inner-workings
of OpenSSL. Here we use the more natural connotation and translate SSLv23
for our purposes to
mean “either SSLv2 or SSLv3.”
Examples
Automatically negotiate the best available encryption protocol supported by the server:
<?php $html = file_get_contents('https://github.com'); ?>
Only allow TLSv1, TLSv1.1, TLSv1.2:
<?php $context = stream_context_create(['ssl' => [ 'crypto_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT ]]); $html = file_get_contents('https://github.com', false, $context); ?>
Use any combination of available flags to limit allowed protocols:
<?php $allowedProtocols = STREAM_CRYPTO_METHOD_SSLv3_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; $context = stream_context_create(['ssl' => [ 'crypto_method' => $allowedProtocols ]]); $fp = fopen('https://www.google.com' . '/', 'r', false, $context); if ($fp) { fpassthru($fp); } ?>
Bind a socket server that only allows TLSv1.1 and TLSv1.2 connections:
<?php $allowedProtocols = STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; $context = stream_context_create(['ssl' => [ 'crypto_method' => $allowedProtocols ]]); $bindFlags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN; $server = stream_socket_server('ssl://127.0.0.1:443', $errno, $errstr, $bindFlags, $context); ?>
Connect using the tls
stream wrapper. The connection will negotiate the best
available protocol of TLSv1, TLSv1.1, TLSv1.2:
<?php $timeout = 42; $connFlags = STREAM_CLIENT_CONNECT; // Works as before $sock = stream_socket_client('tls://github.com:443', $errno, $errstr, $timeout, $connFlags, $context); // Negotiates SSLv3, TLSv1.1 or TLSv1.2 because tls:// default is overridden by the context $context = stream_context_create(['ssl' => [ 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_SSLv3_CLIENT ]]); $sock = stream_socket_client('tls://github.com:443', $errno, $errstr, $timeout, $connFlags, $context); ?>
There is no need to use the deprecated wrappers as the ssl
and tls
wrappers automatically negotiates
the best available protocol. However, for the sake of completeness, note that the following still
works:
<?php $sock = stream_socket_client('sslv3://github.com:443'); // Triggers E_DEPRECATED, still works ?>
Enable crypto on an existing stream. Previously only a single value constant could be used at parameter 3. Flags are now accepted as shown here:
<?php $cryptoMethod = STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; stream_socket_enable_crypto($stream , $enable = TRUE, $cryptoMethod); ?>
Encrypt an existing stream choosing from any protocol we can possibly support using the new catch-all
STREAM_CRYPTO_CLIENT
constant. This method will try all possible protocols:
<?php $sock = stream_socket_client('tcp://github.com:443'); var_dump($sock); // resource(%d) of type (stream) var_dump(stream_socket_enable_crypto($sock, TRUE, STREAM_CRYPTO_CLIENT)); ?>
TL;DR Definitive Progress
Forward Secrecy
Encrypted stream servers now support forward secrecy using ephemeral key exchange via RSA, DH and elliptic curve DH.
Requirements for a secure client transfer prior to PHP 5.6:
Note that this is still insufficient as SAN x509 extension matching is unavailable prior to 5.6.
<?php $context = stream_context_create(array( 'ssl' => array( 'ciphers' => 'DO USERS KNOW WHAT TO PUT HERE? NO.', 'verify_peer' => true, 'cafile' => 'DO USERS KNOW WHAT TO PUT HERE? NO.', 'CN_match' => 'somesite.com', 'disable_compression' => true, 'SNI_enabled' => true, 'SNI_server_name' => 'somesite.com' ) )); $html = file_get_contents('https://somesite.com', null, $context); ?>
Requirements for a secure client transfer in 5.6 without this proposal:
<?php $context = stream_context_create(array( 'ssl' => array( 'ciphers' => 'DO USERS KNOW WHAT TO PUT HERE? NO.', 'disable_compression' => true ) )); $html = file_get_contents('https://somesite.com', null, $context); ?>
Requirements for a secure client transfer in 5.6 if this RFC passes:
Users are encouraged to merge the provided patch and view the HTML returned in the following code which accesses “howsmyssl.com” (a general gauge of the security measures of your client).
<?php $html = file_get_contents('https://howsmyssl.com'); ?>
Removed Features Originally Planned for 5.6
The following stream wrappers currently merged into 5.6 are marked for removal in this proposal:
- tlsv1.1
- tlsv1.2
These wrappers were added for 5.6 prior to the advent of bitwise crypto method specification. The direction chosen in this proposal obviates the need for these wrappers which have never been part of an official release. Their removal has no BC implications.
Backward Incompatible Changes
Most existing code is expected to work without any BC implications. The only source of potential breakage involves the scenario where users connect to servers employing outdated/insecure encryption technologies. For these users the option always exists to manually override secure defaults with insecure settings in the stream context.
Proposed PHP Version
This RFC is proposed for implementation in PHP 5.6.
New Constants
OPENSSL_DEFAULT_STREAM_CIPHERS
Provides userland access to the default cipher list used for stream encryption.
STREAM_CRYPTO_CLIENT
Crypto method interpreted as “any client crypto method we can possibly support.” Applications may use this method for maximum compatibility with SSLv2, SSLv3, TLSv1, TLSv1.1 and TLSv1.2 servers.
STREAM_CRYPTO_SERVER
Crypto method interpreted as “any server crypto method we can possibly support.” Applications may use this method for maximum compatibility with SSLv2, SSLv3, TLSv1, TLSv1.1 and TLSv1.2 clients.
STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
Crypto method flag allowing specific TLSv1 usage in encrypted client streams.
STREAM_CRYPTO_METHOD_TLSv1_0_SERVER
Crypto method flag allowing specific TLSv1 usage in encrypted server streams.
Proposed Voting Choices
- Should PHP implement the recommendations in this proposal as part of 5.6?
Patches and Tests
The proposed changes and tests are available here:
https://github.com/rdlowrey/php-src/compare/php:PHP-5.6...improved-tls-defaults
Implementation
TBD
Vote
Voting period: 2014/02/11 - 2014/02/19
Note that the minor revisions in v0.10 of this RFC were introduced soon after the initial vote announcement (in response to feedback). The changes are cosmetic in relation to the main elements of the RFC. They are noted here to avoid confusion.
Thanks for your time :)
Rejected Features
TBD
Revisions
v0.10 Removed default verify depth setting; tls: wrapper no longer deprecated
v0.9 Added server forward secrecy, updated default cipher list
v0.8 Added new openssl_get_cert_locations()
function
v0.7 Added new “capture_session_meta”
ssl context option
v0.6 Added patch, examples; cipher list updated; new constants; verify_depth, general improvements
v0.5: Removal of protocol-specific stream wrappers now recommended for PHP 6 (was 5.7)
v0.4: Removed recommendations to warn on SSLv2/SSLv3; ssl
wrapper now retained for http fopen
v0.3: Added Stream Wrapper Creep section
v0.2: Update cipher list recommendations and s/DSS/DES/ typo.
v0.1: Original Draft