Create and deploy secure PHARs – Théo Fidry – Medium
source link: https://medium.com/@tfidry/create-and-deploy-secure-phars-c5572f10b4dd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 beforebin/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: phpcache:
directories:
- $HOME/.composer/cachematrix:
fast_finish: true
include:
- php: '7.2'
env:
- EXECUTE_DEPLOYMENT=true
- php: master
allow_failures:
- php: masterbefore_install:
- phpenv config-rm xdebug.ini || true install:
- composer install --no-interaction --no-progress --no-suggest --prefer-distscript:
- 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 anynotifications:
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: phpcache:
directories:
- $HOME/.composer/cachematrix:
fast_finish: true
include:
- php: '7.2'
env:
- EXECUTE_DEPLOYMENT=true
- php: master
allow_failures:
- php: masterbefore_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 .travisinstall:
- composer install --no-interaction --no-progress --no-suggest --prefer-distscript:
- 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 anynotifications:
email: true
A small issue remains here: forks. Indeed when someone will for your repository and open a pull request, Travis will use their fork. As the encrypted data is bound to the main repository, the line openssl aes-256-cbc [...]
will fail. To avoid those kind of failures on forks, a solution is to disable this one for pull requests:
- |
if [ 'false' == "$TRAVIS_PULL_REQUEST" ]; then
openssl aes-256-cbc -K [...]
tar xvf .travis/secrets.tar -C .travis
fi;
Note however that the above will also block your own fork. If you want to be more prevent that, you can go a step further by giving your repository slug:
- |
if [ 'acme/foo' == "$TRAVIS_PULL_REQUEST_SLUG" ]; then
openssl aes-256-cbc -K [...]
tar xvf .travis/secrets.tar -C .travis
fi;
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:
- Matthew Weier O’Phinney for his amazing article on which most of the signing and deployment section are inspired
- Andreas Heigl for his article on signing a PHAR with a GPG key
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
Further edits:
- Added a mention on how to avoid failures on forks due to the encrypted data
Edit as per Jakub Zalas comment: add a mention regarding the configuration of the pull request to not prevent the pull request of your own repository.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK