Securely Distributing PHARs: Pitfalls and Solutions
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.