rfc:improved-tls-defaults

This is an old revision of the document!


Improved TLS Defaults

  • Version: 0.9
  • Date: 2014-01-28
  • Author: Daniel Lowrey, rdlowrey@gmail.com
  • Status: Under Discussion
  • 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

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 munually 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

!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' => [
    'honor_cipher_order' => TRUE
]]);
 
$uri = 'https://www.bankofamerica.com/';
$html = file_get_contents($uri, FALSE, $context);
 
?>

Verify Depth

Proposal

  • Use a default “verify_depth” value of 3

Logic

Users can control how deeply PHP should verify certificates before concluding that the peer's certificate is invalid via the existing “verify_depth” context option. In these scenarios the “depth” is the maximum allowed number of intermediate certificate issuers. So, for example, setting “verify_depth” => 0 means that only self-signed client certificates would be accepted as valid. By default PHP sets no limit on how deeply certificate chains can be verified.

This proposal specifies a default “verify_depth” of 3 if not specified by the user.

Example

<?php
 
// New informational constant exposed to userland
var_dump(OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH); // int(3)
 
$context = stream_context_create(['ssl' => [
    'verify_depth' => 10 // Override the default value
]]);
 
$html = file_get_contents('https://www.bankofamerica.com/', FALSE, $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' => [
    "crypto_method"         => STREAM_CRYPTO_METHOD_TLS_SERVER,
    "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 when client sockets are accepted
// to enable crypto in a non-blocking way ...
 
?>

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.0
  • 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.0
    • tlsv1.1
    • tlsv1.2
  • Deprecate the following stream wrappers in PHP 5.6 with removal (tentatively) scheduled for PHP 6:
    • sslv2
    • sslv3
    • tls

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).

The decision was made to retain ssl as the primary stream wrapper because the general developer population is more familiar with what “SSL” means than “TLS.” Encryption context options are also specified using the “ssl” key so the choice remains consistent with the existing nomenclature. Note again that these stream wrappers are not removed. Their use will trigger an E_DEPRECATED warning but they will continue to function as before.

Note also that though the tls stream wrapper is officially deprecated it 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 utilize TLSv1.

BC Implications of Proposed Stream Wrapper Changes

None.

The only implications for existing code are the E_DEPRECATED messages for the sslv2, sslv3 and tls 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_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 */
} 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', null, $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 (deprecated) tls stream wrapper. The connection will negotiate the best available protocol of TLSv1, TLSv1.1, TLSv1.2. The stream is created successfully and an E_DEPRECATED error is triggered to encourage users to use the catch-all ssl wrapper:

<?php
 
// Triggers E_DEPRECATED, still works
$sock = stream_socket_client('tls://github.com:443');
 
?>

The only wrapper for which the “crypto_method” flags have any effect is ssl. Assigning this context option for the deprecated wrappers has no effect:

<?php
 
$context = stream_context_create(['ssl' => [
    'crypto_method' => STREAM_CRYPTO_METHOD_SSLv3_CLIENT // <-- Won't work; you're stuck with TLS.
]]);
 
// Triggers E_DEPRECATED, still works
$sock = stream_socket_client('tls://github.com:443');
 
?>

There is no need to use the deprecated wrappers as the ssl wrapper 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);
 
?>

TL;DR Definitive Progress

Requirements for a secure 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',
        'verify_depth' => 3,
        'disable_compression' => true,
        'SNI_enabled' => true,
        'SNI_server_name' => 'somesite.com'
    )
));
 
$html = file_get_contents('https://somesite.com', null, $context);
?>

Requirements for a secure transfer in 5.6 without this proposal:

<?php
$context = stream_context_create(array(
    'ssl' => array(
        'ciphers' => 'DO USERS KNOW WHAT TO PUT HERE? NO.',
        'verify_depth' => 3,
        'disable_compression' => true
    )
));
 
$html = file_get_contents('https://somesite.com', null, $context);
?>

Requirements for a secure 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');
?>

Backward Incompatible Changes

Most existing code is expected 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

OPENSSL_DEFAULT_STREAM_VERIFY_DEPTH

Provides userland access to the default verify depth used for stream encryption

STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT

Flag allowing specific TLSv1 usage in encrypted client streams

STREAM_CRYPTO_METHOD_TLSv1_0_SERVER

Flag allowing specific TLSv1 usage in encrypted server streams

Open Issues

  • Feedback?

Proposed Voting Choices

  • Should PHP implement the recommendations in this proposal as part of 5.6?

Patches and Tests

The linked patch is ready to merge against PHP-5.6 and includes several .phpt tests:

https://gist.github.com/rdlowrey/2b3895db9329582d37f6

The changes are also visible here for those who don't like patch files:

https://github.com/rdlowrey/php-src/compare/php:PHP-5.6...improved-tls-defaults

Implementation

TBD

Vote

Voting will begin on Feb. 11

Rejected Features

TBD

Revisions

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

rfc/improved-tls-defaults.1392146528.txt.gz · Last modified: 2017/09/22 13:28 (external edit)