Securely Signing PHP Phar Files With OpenSSL

PHP’s PHAR archives (PHp ARchives, get it?) are a neat development. They’re a way to distribute an entire PHP application as a single archived file that can be executed directly by the PHP intepreter without unarchving them before execution. They’re broadly equivalent to Java’s JAR files and they’re super useful for writing small utilities in PHP.

Starting with PHP 5.3, you can optionally use OpenSSL to sign a Phar archive. The intepreter will now refuse to run the code unless the required public key is present. This provides some security protection; you now know that any subsequent Phars that have been signed with that key are valid updates.

But, unusually, the documentation on Phars, and especially on signing them, are not really all that clear and, frankly, not up to the usual standards of the PHP project. So, here’s a quick primer on building a self-signed Phar file.

Big Fat Warning

I am not a security expert. I know a good bit, and I’ve done a fair bit of research to try to do this the proper way, but I am not warrantying the following information. Moreover, if you see a problem or know of a better way, please contact me and I will update the post.

Prerequisites

  1. PHP of at least 5.3 (really, you should be on at least 5.6 now).
  2. phar.readonly=0 set in php.ini.
  3. The OpenSSL extension installed.
  4. OpenSSL installed on your computer.
  5. A Phar file that you want to sign. Creating a Phar file is beyond the scope of this post.

Step 1: Create a Public/Private Key

Use OpenSSL to create a public/private key pair. This is the fast, but slightly less secure way.

openssl genrsa -out private.pem 4096
openssl rsa -in private.pem -pubout -out public.pem

If you want further security, you could create a key that requires a password to use:

openssl genrsa -des3 -out private.pem 4096
openssl rsa -in private.pem -outform PEM -pubout -out public.pem

The upside of this is it’s now even harder to compromise your key, because a potential attacker would need to both have the private key and know the password to use it. The downside of this is that you will need your password every time you use this key, which precludes the use of automated build tools.

Because the small value of the passworded key vs. the big value of using automated build tools, I usually use the first method.

So you now have 2 keys: a public key, and a private one. Store the private one some place safe. If your private key is compromised, there is no real good way to invalidate it once you have your Phars out in the wild. Your users will need to download a new Phar with a new public key if your key is compromised, not to mention the bad things that could happen if someone could sign updates that look like they come from you. So don’t let that happen. Keep that key safe.

Step 2: Sign the Phar

Okay, you now have a public/private key, how do you sign the Phar? Well, it turns out it’s pretty easy if not terribly well documented. You do it within PHP code!

If you used the first method (the one without passwords), it goes something like this:

<?php
$phar = new Phar("/path/to/your/file.phar");
$private_key = file_get_contents("/path/to/your/private.key");
$phar->setSignatureAlgorithm(Phar::OPENSSL, $private_key);
?>

That’s it! It’s also worth noting that there’s no save() or anything to save the changes to the Phar. The signature is applied when you call the setSignatureAlgorithm() method.

If you used the second method, you have two options. The first option is to temporarily export the passworded private key to an unpassworded one and use the method above (and, certainly, afterward deleting the unpassworded key). To do that, you would issue the following command:

openssl rsa -in private.pem -out private_unencrypted.pem -outform PEM

Then, you would use the first method above. And don’t forget to delete that unpassworded key when you’re done with it!

The other option is to extract the private key in code. It would go something like this. Also, presumably, you would prompt the developer to enter the password and not save it in code (which would completely defeat the purpose of using passwords to begin with).

<?php
$private = openssl_get_privatekey(file_get_contents('private.pem'));
$private_key = '';
openssl_pkey_export($private, $private_key, $password);
$phar = new Phar("/path/to/your/file.phar");
$phar->setSignatureAlgorithm(Phar::OPENSSL, $private_key);
?>

Though, again, you will need to provde the password to export the key.

Step 3: Distributing Your Phar

So what about the public key? So far we haven’t done anything with it, but that’s about to change. And this is important, so pay attention here.

The public key must be named the same name as the Phar file, with .pubkey added, and must be in the same directory as your Phar. If your Phar is called myphar.phar, your public key distributed with your phar would be myphar.phar.pubkey. This is in the docs, but it’s pretty deeply buried and not very clear - I actually discovered this by reading the relevant PHP C source code after I couldn’t figure out why it wasn’t working.

If you don’t do this, your users will not be able to run your Phar. The PHP engine will refuse to run the code.

So your final distribution would contain the folloing files:

  • file.phar - Your signed Phar archive.
  • file.phar.pubkey - The public key to verify the signed archive against.

For most of my projects that use Phar, I have a Robo task that builds the Phar, signs it, sets it executable, packages it up in a Zip file and deploys the Zip file, the Phar itself and the public key to a distribution server.

In my next post, I’ll talk about using this to create user-updatable Phar files that leverage this signing method to insure safety for your users.

Did something I wrote help you out?

That's great! I don't earn any money from this site - I run no ads, sell no products and participate in no affiliate programs. I do this solely because it's fun; I enjoy writing and sharing what I learn.

COVID-19 has taken the world by storm and left a lot of brokenness in its wake. A lot of people are suffering. If you feel so inclined, please make a donation to your local food bank or medical charity. Order take-out from your local Chinese restaurant. Help buy groceries for an unemployed friend. Help people make it through to the other side.

But if you found this article helpful and you really feel like donating to me specifically, you can do so below.

Read More

Security on Shared Hosts

Shared hosts are a reality for many small businesses or businesses that aren’t oriented around moving massive amounts of data. This is a given - we can’t all afford racks full of dedicated servers. With that in mind, I would urge people to be more careful about what they do on shared hosting accounts. You should assume that anything you do is being watched. Take, for example, the /tmp directory. I was doing some work for a friend this weekend whose account is housed on the servers of a certain very large hosting company. While tweaking some of his scripts, I noticed via phpinfo() that sessions were file-based and were being stored in /tmp. This made me curious as to whether any of that session data could possibly be available for public viewing. My first move was to simply try FTP’ing up and CD’ing to /tmp directory. No go - they have the FTP accounts chrooted into a jail, so the obvious door is closed. However, the accounts have PHP installed, so I can do something like this in a PHP script: <?php system("ls -al /tmp"); ?> With this little bit of code, I can look into the tmp directory even if my FTP login is chrooted. Fortunately, sessions on this host are 600, so they’re not publically readable -  this was my primary concern and the reason I took some time to check this out. But people are putting lots of things into the tmp directory with the misguided idea that it is their private temporary file dump, including one idiot who put a month’s worth of PayPal transaction data into tmp and left it 644 so that it was publically viewable. Now, I’m a nice guy and the only thing I’m going to do with this information is laugh at it. But keeping in mind how dirt cheap hosting accounts are, there’s not a high entry barrier for someone with fewer scruples. The key thing to remember is that, if you need temporary file storage on a shared host, do it someplace less obvious, set the permissions so that only you can read/write to it (600), and clean up by deleting files as soon as you possibly can.