Create and deploy secure PHARs

For those who are not familiar with it, PHAR (Php Archive) is analogous to the JAR file concept but for PHP. It allows you to package an application into a single file making it convenient to deploy or distribute. This used to be very convenient for deploying an application over FTP as there is only a single file to replace. Luckily, we don’t have to do that (FTP deployment) anymore (if not, I’m sorry for you).

So what are PHARs useful for then? Well still the same thing: packaging applications. Although not many people may want to use this technique for web applications, it is still extremely useful for console applications.

State of the art

Installing a PHAR

There is currently several ways:

  • Use an installer, i.e. a script to install the PHAR
  • Download the PHAR directly
  • Use a PHAR manager like PHIVE

Installation with an installer

How the installer works depends from a tool to another as each installation script can differ. For example the current way to download the Composer PHAR is:

$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php -r "if (hash_file('SHA384', 'composer-setup.php') === '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"

Another example with the Symfony Installer:

$ sudo curl -LsS https://symfony.com/installer -o /usr/local/bin/symfony
$ sudo chmod a+x /usr/local/bin/symfony

Installation by downloading the PHAR directly

Some projects like PHPUnit or PsySH opt for a simpler (although less secure) way which is downloading the PHAR directly:

$ wget https://phar.phpunit.de/phpunit-6.5.phar
$ chmod +x phpunit-6.5.phar
$ sudo mv phpunit-6.5.phar /usr/local/bin/phpunit
$ phpunit --version

Installation with PHIVE

PHIVE is a convenient tool to download and verify PHARs, once installed requiring a PHAR is as easy as:

$ phive install phpunit

Or if you didn’t register your project in their database in which case you cannot use an alias like above, you can always install it directly via the GitHub URL:

$ phive install phpDocumentor/phpDocumentor2

Building a PHAR

PHP provides tons of functionalities around PHARs. The issue is that the manual is not super detailed still and building a PHAR remains a cumbersome task. You can find examples of how to do it in Composer and PsySH. Luckily there is now the box project which makes it far easier. All you have to do is create a configuration file box.json.dist which looks like this:

{
"chmod": "0755",
"main": "bin/command.php",
"output": "bin/command.phar",

"directories": ["src"],
"finder": [
{
"name": "*.php",
"exclude": ["test", "tests"],
"in": "vendor"
}
],
    "stub": true
}

And once you have created this file and installed box, you can build your PHAR with:

$ box build

You will now have the PHAR file bin/command.phar which you can execute either by calling it with php like any PHP file:

$ php bin/command.phar

Or make it executable and execute it directly:

$ chmod u+x bin/command.phar # Make the PHAR executable
$ bin/command.phar # Execute it

Note that the previous way still works even if the file is executable. This is quite useful if you want to pass some options like disabling the garbage collection when you can afford it to speed things up:

$ php -d zend.gc_enable=0 bin/command.phar

As they are binary artefacts, it is good practice to not commit PHARs so you should add them to your project .gitignore:

$ echo "/bin/command.phar" >> .gitignore

It is also worth mentioning that there is also the PHAR CLI provided by the PHAR extension:

$ man phar

As well as other PHAR building tools such as theseer/Autoload.

Updating a PHAR

There are two simple ways to update a PHAR:

  • Using PHIVE
  • Using a self-update command

PHIVE

You can use the phive update command to securely update a PHAR.

Self-update command

There are several projects providing helpers for creating self-update commands. The most popular one is Padraìc’s phar-updater project which has been moved under the Humbug umbrella. Here is an example with a Symfony command:

<?php declare(strict_types=1);

namespace Acme\PharDemo\Console\Command;

use Humbug\SelfUpdate\Updater;
use PHAR;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

final class SelfUpdateCommand extends Command
{
private $updater;

/**
*
@inheritdoc
*/
public function __construct(Updater $updater)
{
parent::__construct();

// Use dependency injection instead, this is just to show
// you how the updater is configured
$this->updater = new Updater('bin/command.phar');
$this->updater->setStrategy(Updater::STRATEGY_GITHUB);
$this->updater->getStrategy()->setPackageName('acme/phar-demo');
$this->updater->getStrategy()->setPharName('command.phar');
}

/**
*
@inheritdoc
*/
protected function configure(): void
{
$this
->setName('self-update')
->setDescription(sprintf(
'Update %s to most recent stable build.',
$this->getLocalPharName()
))
;
}

/**
*
@inheritdoc
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$result = $this->updater->update();

if ($result) {
$io->success(
sprintf(
'Your PHAR has been updated from "%s" to "%s".',
$this->updater->getOldVersion(),
$this->updater->getNewVersion()
)
);
} else {
$io->success('Your PHAR is already up to date.');
}

return 0;
}

private function getLocalPharName(): string
{
return basename(PHAR::running());
}
}

Signing a PHAR

Signing a PHAR ensures that the PHAR you are using cannot be execute if its content has been changed. While some vulnerability remains, this is a solid approach for most usages.

The first step is to create an OpenSSL private key:

$ openssl genrsa -des3 -out acme-phar-private.pem 4096

The above will prompt you for a passphrase which is used to encrypt the key. To be able to automate the release process (see the next part), you will need to strip that key from any passphrase:

# Save the passphrased protected key
$ cp acme-phar-private.pem acme-phar-private.pem.passphrase-protected
$ openssl rsa -in acme-phar-private.pem -out acme-phar-private-nopassphrase.pem
$ cp acme-phar-private-nopassphrase.pem acme-phar-private.pem

If you are not planning to automate the deployment process, just keep your private key somewhere and make sure to not commit it. Here, we will prepare it for the automated deployment and create a .travis/ subdirectory in your project with a .gitkeep file. Put the private key in it and then add that file to your project’s .gitignore to avoid to push an unencrypted key to the repository:

$ mkdir .travis
$ mv acme-phar-private.pem .travis/
$ echo ".travis/phar-private.pem" >> .gitignore

Box also support PHAR signing, so we can tweak the box.json.dist configuration file to add it:

{
"output": "bin/command.phar",
...
"algorithm": "OPENSSL",
"key": ".travis/acme-phar-private.pem"
}

After building the PHAR with the box build command, you will now find two files:

  • bin/command.phar which is the PHAR build as before
  • bin/command.phar.pubkey which is the public key derived from the private key

Now $ bin/command.phar will fail whenever the public key is absent, wrong or when the PHAR has been tempered with.

Like the PHAR the public key should be ignored for git:

$ echo "/bin/command.phar" >> .gitignore
$ echo "/bin/command.phar.pubkey" >> .gitignore

Note that now you will need a private key even if you want to build the PHAR for development purposes. Do disable it for development, an easy solution for now is to create a box.json file in which we remove the signing part. You can also check. For PHP-Scoper for example, we leverage Makefile for that:

box.json: box.json.dist
cat box.json.dist | sed -E 's/\"key\": \".+\",//g' | sed -E 's/\"algorithm\": \".+\",//g' > box.json

You can also encrypt your PHAR with a GPG key instead.

PHAR auto-release

Automating the release process depends a lot of the Continuous Integration tool you are using. The following example will be for Travis CI. In this part, we’ll assume Travis is already enabled for our repository.

Configuring Travis

You should add a .travis.yml file to your project, if you haven't already. Here's a basic template:

language: php
cache:
directories:
- $HOME/.composer/cache
matrix:
fast_finish: true
include:
- php: '7.2'
env:
- EXECUTE_DEPLOYMENT=true
- php: master
allow_failures:
- php: master
before_install:
-
phpenv config-rm xdebug.ini || true
install:
- composer install
--no-interaction --no-progress --no-suggest --prefer-dist
script:
- vendor/bin/phpunit # Execute your tests
- vendor/bin/box build # Build your PHAR
# Execute some end-to-end tests with your PHAR if you have any
notifications:
email: true

Commit and push that file, and you should see your first build appear on Travis-CI.

Adding encrypted files to Travis

Travis-CI provides a number of facilities for encrypting secrets that you wish to utilize during the build process. In our case, we need to provide encrypted files.

Interestingly, due to some issues with OpenSSL and the way the support is implemented in Travis-CI, you can only encrypt a single file. Thus, if you have multiple files, you need create an archive of them and encrypt that.

$ cd .travis
$ tar cvf secrets.tar *.pem
$ cd ..

This will create the file .travis/secrets.tar.

Now, we need to encrypt the file. To do this, you will need to install the travis gem, login and encrypt them:

$ gem install travis
$ travis login
$ travis encrypt-file .travis/secrets.tar \
.travis/secrets.tar.enc --add

This will create a new file .travis/secrets.tar.enc and add an entry to your.travis.yml's before_install section that will decrypt the file; this means that your code and scripts on Travis-CI can then rely on .travis/secrets.tar being available.

Note that when you use the -add flag and travis, it rewrites your .travis.yml file which might alter the spacings.

We’ll add the .travis/secrets.tar.enc file to the repository, and omit.travis/secrets.tar:

$ git add .travis/secrets.tar.enc
$ echo ".travis/secrets.tar" >> .gitignore

When a build is triggered on Travis-CI now, it will decrypt this file before any of our build processes are triggered, allowing us access to those secrets!

Your .travis.yml file should now look similar to this:

language: php
cache:
directories:
- $HOME/.composer/cache
matrix:
fast_finish: true
include:
- php: '7.2'
env:
- EXECUTE_DEPLOYMENT=true
- php: master
allow_failures:
- php: master
before_install:
-
phpenv config-rm xdebug.ini || true
# The part added by Travis:
- openssl aes-256-cbc -K $encrypted_smth_key -iv $encrypted_smth_iv -in .travis/secrets.tar.enc -out .travis/secrets.tar -d
- tar xvf .travis/secrets.tar -C .travis
install:
- composer install
--no-interaction --no-progress --no-suggest --prefer-dist
script:
- vendor/bin/phpunit # Execute your tests
- vendor/bin/box build # Build your PHAR
# Execute some end-to-end tests with your PHAR if you have any
notifications:
email: true

Add the deployment config

Now that we have our secrets securely available on Travis-CI, we can configure the deployment entry of our .travis.yml file to publish our signed PHAR with GitHub releases.

As per the doc, you can use the travis setup releases command. This will create a deploy entry in your .travis.yml that you can then tweak to your needs:

deploy:
provider:
releases
api_key:
secure:
YOUR_API_KEY_ENCRYPTED
file:
- bin/command.phar
- bin/command.phar.pubkey
skip_cleanup: true
on:
tags:
true
repo: acme/phar-demo
condition: "$EXECUTE_DEPLOYMENT"

Et voilà!

You can also find an alternative way in Andreas Heigl’s article by leveraging encrypted environment variables instead.

If you wish to know more about PHARs and the tooling around them I recommend you to check this article PHARs Roadmap.

Credits

A big thanks to:


Edits as per Arne Blankerts comment:

  • Corrected some instructions regarding the usage of Phive
  • Mentioned other PHAR building tools
  • Added link to the PHAR roadmap