A few weeks back I wrote a piece about updating PHARs in-situ, what we’ve taken to calling “self-updating”. In that article, I touched on making Transport Layer Security (TLS, formerly SSL) enforcement one of the central objectives of a self-updating process. In several other discussions, I started using the phrase “Lowest Common Insecure Denominator” as a label for when a process, which should be subject to TLS verification, has that verification omitted or disabled to serve a category of user with poorly configured PHP installations.
This is not a novel or even TLS-only concept. All that the phrase means is that, to maximise users and minimise friction, programmers will be forever motivated to do away with security features that a significant minority cannot support by default. In the case of PHP users on Windows, this may include not having openssl or curl installed. Without either of these options, TLS verification in PHP becomes impossible without looking outside PHP (e.g. locally available system commands).
The problem is that while programming to the Lowest Common Denominator is fine for many things, doing so to the point of maintaining active security vulnerabilities is not. Let’s take the simple example of Composer. It’s an incredible tool, used by most PHP programmers I know, but it can’t perform TLS verification worth a damn despite operating primarily over HTTPS URLs. On Reddit, there is a another tool just announced which relies on Composer to update application modules. That inherits the same vulnerability by depending on Composer in a live server setting. So too will other Composer dependent tools merely by inheriting from or reusing its download classes. In time, you finally have people seeking refuge in authority because Composer does this, and look, everyone and their pet hamster still uses it!
There’s A Topic In Here Somewhere
Much as I did around writing phar-updater and, more importantly, documenting the reasoning behind a tool that enforces TLS and supports openssl signing as a first citizen, I’d like to drill down into the specifics of how to approach this problem. It’s not an insurmountable one assuming you accept some basic ideas:
- You should never knowingly distribute insecure code.
- You should accept responsibility for reported vulnerabilities.
- You should make every effort to fix vulnerabilities within a reasonable time.
- You should responsibly disclose vulnerabilities and fixes to the public.
These four ideas are self-explanatory as the guiding principles that any good security policy is founded upon. When you violate them, you earn general mistrust and reputational damage when your users either figure out that violations occurred, or that those violations contributed towards the worst case scenario: getting hacked and all the ugly outcomes that follow. You only need to go on Reddit and other news sites to find that Magento’s reputation is currently being ripped to shreds over failing to uphold these principles recently.
So, given something like an application where the expectation is that everyone will install it, whether it be on Ubuntu, Windows, or Terminator-X45, how does one go about implementing TLS verification as securely as possible without being overly burdensome on programmers? Is it even possible?
Step 1: Implement TLS Verification
In keeping with those four ideas from earlier, the first course of action is to just implement TLS verification and get a handle on the consequences. Foisting a security vulnerability onto all members without their consent is irresponsible programming and should never be tolerated by the community.
It’s essential to reiterate that Insufficient Transport Layer Protection is a security vulnerability, making it possible for attackers to perform Man-In-The-Middle (MITM) attacks (this applies to intranets as much as on the internet). If it’s located and reported, it falls under your published security policy (if any) and the four central principles expressed earlier. There is a reason why URLs on the internet are prefixed with HTTPS.
We like to think of PHP as the programming language of the web, yet we continue to struggle and fight against a 20 year old protocol that underpins the security of users on that web.
Step 2: Identify The Consequences
All programmers spend a decent amount of time problem solving, and that should come into play now. For most people, there will be no unwanted consequences. They will have openssl/curl installed, and their operating system will have the necessary Certificate Authority (CA) certificates available. The only errors that they will experience will be the usual HTTP fare, and infrequent SSL errors for misconfigured remote servers (not the local system). Attempted MITM attacks will also, very obviously, generate errors assuming the TLS implementation and its dependencies are sound.
The less desirable consequences will then make themselves known. The most commonly quoted one is that Windows users will encounter errors because their local PHP does not have openssl or curl enabled. Other common issues are errors around locating the CA certificates necessary when verifying the remote server’s SSL certificate.
You now have two choices in implementing TLS. Enforce it or disable it.
First of all, you can’t just disable it because it would then put you in the position of deliberately introducing a security vulnerability. Leaving it enabled is not the end of the world. Just because some users have a poorly configured PHP, it does not immediately follow that all users should have their security compromised by default.
The more logical approach is to assess the local system prior to making remote requests. Those who pass muster will be fully secured, and those who don’t? We’ll get to them…
Step 3: Document Solutions & Stand Over Your Dependencies
TLS verification in PHP requires openssl or curl (or both depending on the application dependencies). Short of falling back to secure local system options on the command line, this is an unavoidable part of programming in PHP. So, when users don’t have the requisite extensions, and you have no other fallback to hand, you should simply start by telling them this.
When it comes to missing extensions, the solution is generally just a minor php.ini edit away. On Windows, it’s often just adding or uncommenting a line like “extension=php_openssl.dll”. Don’t only give users an “openssl not installed”, or worse, a puzzling one liner lifted from a PHP error message. Provide some information as to what is missing, why it is required, and a link as to where to find help.
This brings us to documentation. Most of the dependency issues have very simple solutions, editing a line or two in php.ini, or installing/downloading a CA certificate pack. Those extension DLL files are normally available regardless of how you get PHP. You can summarise the common solutions on your website or wiki and include the link in any error or feedback messages within the application output.
Step 4: Let Loose the Big Red Box of Doom?
To this point, you’ve avoided introducing a security vulnerability and have done your best to enable users to fix their dependency and configuration issues. They still want to use your application despite not following your recommendations. Perhaps it’s time to make it possible for users to shoot themselves in the foot without your assistance?
In a CLI application, for example, you can create a new “—disable-tls” flag which disables TLS protections when set. Whenever it is used, a very obvious, very unavoidable and very red box is displayed, informing the user that TLS protections have been disabled for the current command.
Text along the lines of “The end of the world is nigh!” would probably be too much. Mentioning that they are now vulnerable to Man-In-The-Middle attacks would simply be fact.
At its crux, this article is as much about user consent as anything else. Distributing code which simply turns off all TLS protection (or indeed, any security protection) without a user’s knowledge is irresponsible. Enforcing it but allowing for users to opt-out of that protection after seeing an informative warning is not quite as dire. Some security professionals will still be unhappy with the idea of ignorant users just wanting all the errors to go away and being able to achieve that, but this is actually an approach that exists in modern browsers whenever an invalid SSL certificate or TLS error is experienced.
At some point, users need to take on a bit of the responsibility for their own protection.
Doubtlessly, this would inconvenience some users. Extra settings or CLI flags might not be immediately obvious, digging through a php.ini to find those extension lines is an inconvenience, and red warning boxes everywhere can be annoying, but the alternatives as they popularly exist in PHP today need to go extinct. This idea of disabling security by default, of programming to the Lowest Common Insecure Denominator, without anyone’s consent or knowledge is neither responsible nor sustainable.
As Gandalf would say, “Keep it secret, keep it safe”. Just putting it on a table in plain view for the Sackville-Bagginses to steal is neither!
The PHAR ecosystem has become a separate distribution mechanism for PHP code, distinct from what we usually consider PHP packages via PEAR and Composer. However, they still suffer from all of the same problems, namely the persisting whiff of security weaknesses in how their distribution is designed.
What exactly can go wrong when distributing any sort of PHAR?
- Downloading PHARs from a HTTP URL not protected by TLS.
- Downloading PHARs from a HTTPS URL with TLS verification disabled.
- Downloading PHARs which are unsigned by the authors.
- Downloading any PHAR “installer” unnecessarily.
All of the above introduce an element of risk that the code you receive is not actually the code the author intended to distribute, i.e. it may decide to go do some crazy things that spell bad news when executed. A hacker could mount a Man-In-The-Middle attack on your connection to the PHAR server, or compromise the PHAR server and replace the file, or employ some DNS spoofing trickery to redirect download requests to their server.
I’ve started to distribute a CLI app phar of my own recently for Humbug, so I had to go and solve these problems and make installing, and updating, that phar both simple and secure. Here’s the outline of the solution I’ve arrived at which is quite self-evident.
- Distribute the PHAR over HTTPS
- Enforce TLS Verification
- Sign your PHAR with a private key
- Avoid PHAR Installer scripts
- Manage Self Updates Securely
- Do all of this consistently
Some details and a discussion on each point…
Distribute the PHAR over HTTPS
If you really don’t already have a TLS enabled download location, you can avail yourself of Github.io which supports HTTPS URLs. I’m using this for Humbug‘s development builds. You can also use Github Releases for your project and attach the phars there for new versions. If you do need to host the PHAR on your own server, get a TLS certificate for your domain.
Enforce TLS verification
PHP supports TLS verification out of the box, for the most part. It was disabled by default until PHP 5.6. Enforce it! If a user cannot make a simple request to a simple HTTPS URL, then their server is quite obviously misconfigured. That is not your problem, so don’t make it your problem. You use HTTPS, you enforce TLS, and other programmers should be more than capable of fixing their own stuff. Insecure broken systems are not the lowest common denominate you should be targeting.
Enabling TLS verification for PHP’s stream functions, e.g. file_get_contents(), is basically a disaster waiting to happen because its configuration can be fairly long winded to get just right. As something of a shim, I’ve created the humbug_file_contents package which has a ready to roll TLS-loving function that can replace file_get_contents() transparently, but only when it detects a PHP version less than 5.6.
PHP 5.6 introduced significant TLS improvements which were enabled by default. In certain areas, it actually exceeds what might be expected from other options, and it’s certainly better than any combination of pre-5.6 options can currently achieve (even humbug_get_contents() can only rate a “Needs Improvement” in its remote tests which is good enough until PHP 5.5 goes EOL).
TLS verification using stream enabled functions does require openssl. Document it as a required dependency.
Sign your PHAR with a private key
All PHARs should be signed using RSA private keys through openssl. Run the following commands in bash to create a 2048 bit key and use a strong password when prompted.
openssl genrsa -des3 -out phar-private.pem 2048 openssl rsa -in private.pem -outform PEM -pubout -out phar-public.pem
You just created phar-private.pem and phar-public.pem files. The private key is encrypted (hence the password) and the public key is unencrypted. Keep the private key safely offline and do not lose it. If you’re worried about softcopy backups, you can print a hardcopy and put that in a secure location. Do not strip the password from the private key unless you want to reset it!
If your phar file were called foo.phar, you would need to distribute the public key alongside it as foo.phar.pubkey. When someone tries to use a PHAR that’s signed, that’s how PHP locates the public key. For obvious reasons, the public key cannot be part of the PHAR itself.
Never distribute the private key. It shouldn’t even be anywhere near a remote server. It’s an offline key.
Need to automate development versions of PHARs? I’ll talk about that another day, but it’s doable by using your offline key to sign some metadata that authorises a second private key (on a server without password) to sign PHARs as a delegate. It’s not covered in this article, and any use of these approaches have management weaknesses (e.g. revoking stolen or abused keys is not straightforward). Let’s stick to the basics for now.
The public key should also be prominently available online. If I download a copy, there should be a few options to manually verify that the key is correct, e.g. manual and README on different domains. I have not done this for Humbug yet – it’s a simple but easy to forget way to advertise your genuine key.
The act of signing a PHAR requires setting a signature algorithm, assuming the PHP manual ever lets you. PHP’s documentation for PHARs is often outdated and incomplete. The few steps needed are, given a PHAR foo.phar represented by $phar in this PHP snippet:
/** Get private key contents as $privateKey using openssl. Prompt for the password from a script – do not include it in any configuration. */ $phar->setSignatureAlgorithm(\Phar::OPENSSL, $privateKey); copy('/path/to/phar-public.pem', '/path/to/foo.phar.pubkey'); /** wrap up any last PHAR stuff */
There are other PHAR signature types, but these do not use keys, and so they are not performing the same function as key based signing. If the code above is scary, The box library can do the signing and PHAR compiling for you (you can git clone it ;)).
Avoid PHAR Installer scripts
In the race to make users happy, some PHARs come with an unsigned installation routine packaged as a PHP file which is downloaded and passed directly to the PHP interpreter on the command line. This is usually completely unnecessary. As PHARs are self-contained files, they require no “installation”. Such installers verify if a system meets the necessary dependencies and settings, and then initiate the download (often with TLS disabled unnoticed).
Document your dependencies, and your needed PHP INI settings. Let users check if their systems support your PHARs documented requirements. Then provide them with the URLs to the PHAR and pubkey files to download using wget, curl or their browser.
This misconception of installers being required is reinforced if the URL to your actual PHAR and pubkey (the only two files I need on my filesystem) are not provided without having to open the installer and read code.
Manage Self-Updates securely
This is the process of checking for new versions, downloading them, and replacing the old version. All of the above rules from earlier still apply with a few key twists (such as the irony that a self-update command is suspiciously like an installer!). Luckily, you downloaded a signed PHAR over a TLS protected HTTPS URL (fingers crossed), so you have slightly more trust that it won’t rampage through your system compared to a PHP script piped to PHP from an unprotected URL.
Whenever you download a PHAR that is expected to be genuine and properly signed, you always face a few tasks. It’s generally easier when done manually when getting it the first time because a) you don’t have the public key already so you must choose whether or not to trust it (Trust On First Use - TOFU), b) you should be requesting two specific URLs manually and not relying on potentially egregious installer scripts, and c) this means you should have better than even odds of getting the code you want if the self-updating routine is decently designed.
In other words, you trust the existing PHAR, and you can enforce that trust across all future updates by reusing the existing public key.
For this I’m in the process of writing the phar-updater package. Right now, it supports a basic SHA-1 synchronisation strategy where the PHAR self-updates when a remote SHA-1 version file updates, thus indicating a new PHAR build was released. It then downloads the new PHAR, runs validation routines to ensure it’s genuine (e.g. was signed by same private key as the original), and then replaces the original PHAR in-situ. Of course, it also enforces TLS.
Do all of the above consistently
I’ve just written a big article for two relatively simple to implement things: TLS enforcement and PHAR signing with RSA private keys – all with self-updating support if needed. The outcome, to a user, is that they end up with two files instead of one and a nice self-update option. This is not an outrageous outcome to introducing proper security on PHAR downloads. Go forth and do it for all PHARs. Help create an environment where distributing and installing code in secure ways is the normal expected thing to do.
Those who would prefer a GPG approach can run with that also. There is nothing to prevent distributing current.phar.asc and current.phar.pubkey.asc files for that purpose. However, GPG is a manual checking process whereas using RSA keys can be automated and is performed for all PHAR uses as standard. Neither approach excludes the other.