Dev Blog

Use case presented here is for signing and verifying files, however by tweaking commands it is possible to encrypt and decrypt files too. Generally it's about GPG batch operations.

The idea with GPG (or PGP, which is compatible) is that keys are securely stored in user home dir. PHP comes with PECL extension for providing GPG operations, however it is using and approach where application manages the secret and public key. This could compromise security, as these keys would be probably stored in some PHP variables. The workaround for this could be set up an key pair for web server user.

Transferring key pair

In case you need key pair, issue following command:

gpg --full-gen-key

This will guide you through process of generating key. We will not came into details on generating keys, as it's a side and well known topic.

For batch operations key might be created or exported without password. As it anyway need to be stored somewhere. Password can be provided at command line or possibly by GPG agent.

Keys can be generated directly on server, however when having more than one server, keys need to be distributed. 

Exporting keys

To export public and private keys we'll use following command:

gpg --export key-email@example.com > key-name.pub

By default GPG outputs key to console, so we redirect output to file with > operator.

The similar command is for a private key:

gpg --export-secret-keys key-email@example.com > key-name.asc

Transfer these files with encrypted connection to server. This could be done with GUI client, like FileZilla.

Importing keys

To import keys for web server user - we assume here www-data - we need to create home directory with proper permission first, if that user haven't it already:

sudo mkdir -p /home/www-data/.gnupg
sudo chown -R www-data:www-data /home/www-data/.gnupg sudo chmod 700 /home/www-data/.gnupg

To import keys we'll again use sudo command with -u option to work on behalf of www-data. However even with sudo, the GPG will try to import keys into current user key ring. To overcome this, there we need to pass --homedir parameter to GPG:

In case of permission denied errors or No such file or directory errors - you might need to actually log on as a web server user (ie www-data) and issue import and verify commands without sudo - as current user.

sudo -u www-data gpg --homedir /home/www-data/.gnupg --import key-name.pub

As a side note ensure that file key-name.pub is readable by www-data. The command to import the private key is the same, except as a parameter we pass a private key file name:

sudo -u www-data gpg --homedir /home/www-data/.gnupg --import key-name.asc

Verify Transfer

Keys should be now imported. Note that when using with sudo, the --homedir parameter need to be added. To verify that keys have ben imported properly, list them:

sudo -u www-data gpg --homedir /home/www-data/.gnupg --list-keys
sudo -u www-data gpg --homedir /home/www-data/.gnupg --list-secret-keys

Next step is to sign and verify signature as www-data user with imported key. To ensure that exact key is used, we'll use the -u argument, to choose key manually and sign file named test.file:

sudo -u www-data gpg --homedir /home/www-data/.gnupg -u key-email@example.com --output test.sig --detach-sig test.file

The commands started to become lengthy. However parts of the commands are repeatable, so so simple PHP command builder can be used later. For now, let's verify results. First of all, there should be created new file - test.sig - containing binary data with signature. To verify signature call gpg command with --verify option:

sudo -u www-data gpg --homedir '/home/www-data/.gnupg' -u key-email@example.com --verify test.sig test.file
Most likely there will be warning that the key is untrusted, but should verify signature.

Using with PHP

After all those commands, the GPG is ready to be used with PHP. Now is the easy part - just execute proper shell commands. There is no need for external library. As the key ring is owned by web server user commands will use previously imported key. But it is good to point to exact key with -u option. Part of command will be the same for all operations. Set key, add --batch and --quiet options to make gpg command non-interactive and select key.

The idea is to create array with arguments and then join it. Example "class", which is in fact functions container:

class Signatures  
{
    public static function sign($fileName, $sigName)
    {
        $args = self::makeArgs();
        $args[] = '--output ' . escapeshellarg($sigName);
        $args[] = '--detach-sig ';
        $cmd = sprintf('gpg %s %s', implode(' ', $args), escapeshellarg($fileName));
        return self::run($cmd);
    }

    public static function verify($fileName, $sigName)
    {
        $args = self::makeArgs();
        $args[] = '--verify ' . escapeshellarg($sigName);
        $cmd = sprintf('gpg %s %s', implode(' ', $args), escapeshellarg($fileName));
        return self::run($cmd);
    }

    private static run($cmd)
    {
        $result = null;
        $output = [];
        exec($cmd, $output, $result);
        // Return true on successfull command invocation
        return $result === 0;
    }

    private static function makeArgs()
    {
        $args = [
            '--batch',
            '--quiet',
            '--homedir /home/www-data/.gnupg',
            '-u key-email@example.com',
        ];
        return $args;
    }
}

Above example has common parameters hardcoded, just for sake of simplicity. This could be simplified even more, as there are a bit of redundant code. To test is as a web user, either run it through browser or as command line with sudo -u www-data.