Skip to content

Instantly share code, notes, and snippets.

@julienfastre
Last active September 22, 2015 18:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save julienfastre/e380f19a1e31b737418c to your computer and use it in GitHub Desktop.
Save julienfastre/e380f19a1e31b737418c to your computer and use it in GitHub Desktop.
POC: cryptographic signature in composer package

Include cryptographic signature in composer package

See this issue for more information : composer/composer#38

THIS IS A PROOF OF CONCEPT. If the test does work, this could become a composer plugin.

Prerequesites: installation of gnupg (PECL extension)

Prerequesites : You must install the gnupg extension on php. See the php manual about gnupg installation, the php manual about installing PECL extensions. On Ubuntu, I had to install the package libgpgme11-dev (some depedencies were also installed) and then run sudo pecl install gnupg. I had to register the extension using sudo su -c 'echo "extension=gnupg.so" >> /etc/php5/mods-available/gnupg.ini' root and then enable it with sudo php5enmod gnupg.

Installation (you have gnupg on your machine)

Clone this gist using

git@gist.github.com:e380f19a1e31b737418c.git composer-signature-poc

Usage for signing

  1. cd into your package directory (the directory where you will find the composer.json file)
  2. execute the sign script (here, ABCDEF123 is the key's fingerprint):
php /path/to/your/composer-signature-poc/sign.php ABCDEF123

This will scan all files, calculate a sha1sum for each file, dump a json representation's string of the sha1, sign the string and dump it into composer.sha1.json file.

Usage for verification

  1. grep the key from a keyserver : gpg2 --recv-keys ABCDEF123 where ABCDEF123 is the key fingerprint
  2. cd into your package directory (the directory where you will find the composer.json file)
  3. execute the verify script: php /path/to/your/composer-signature-poc/verify.php <the signing key fingerprint>

This will output the verification and, eventually, a success/failure message.

I want to test !

Currently, I have signed one of our package: chill-project/main.

Please, download it and check the signature.

The package is signed by the key 52577F34.

So...

  1. Check that you have installed gnupg pecl extension for php. Instructions for Ubuntu/Debian : sudo apt-get install libgpgme11-dev, sudo pecl install gnupg, sudo su -c 'echo "extension=gnupg.so" >> /etc/php5/mods-available/gnupg.ini' root, sudo php5enmod gnupg (those instructions weren't tested on multiple machine. If you experience a different way, please let me know at julien [AT ] champs-libres [DOT] coop)
  2. git clone git@github.com:Chill-project/main --branch signature_poc --single-branch chill-main
  3. git clone git@gist.github.com:e380f19a1e31b737418c.git composer-signature-poc
  4. cd chill-main
  5. gpg2 --recv-keys 52577F34
  6. php ./../composer-signature-poc/verify.php 52577F34
#!/usr/bin/php
<?php
$scriptName = $argv[0];
$helpText = <<<EOF
This script create a 'composer.sha1.json' file in the current directory. The file is signed by the specified gpg key
Usage: php $scriptName <gpg_key_id>
-h --help Show this help
In a near future it will be possible to skip more directories.
EOF;
if (in_array('-h', $argv) OR in_array('--help', $argv) OR count($argv) == 1) {
echo $helpText;
}
$directory = '.';
$gpgKey = $argv[1];
$skippedPaths = array('/(\.\/)?(.git)/', '/(\.\/)?(bin)/', '/(\.\/)?(vendor)/'); //currently, skip git directory, bin directory, vendor directory
//prepare gpg
$gpg = new gnupg();
$gpg->addsignkey($gpgKey);
// prepare iteration on files
$directoryIterator = new \RecursiveDirectoryIterator($directory, FilesystemIterator::CURRENT_AS_FILEINFO);
$recursiveIterator = new \RecursiveIteratorIterator($directoryIterator);
// iterate on file
$files = array(); // info will be stored into file array
foreach ($recursiveIterator as $fileinfo) {
// we skip ., .., and composer.sha1.json
if(in_array($fileinfo->getFilename(), array('.', '..', 'composer.sha1.json'))) {
continue;
}
// skip links -- Should we do this ?
if ($fileinfo->isLink()) {
continue;
}
// skip directory
foreach ($skippedPaths as $pattern) {
if (preg_match($pattern, $fileinfo->getPathname()) === 1) {
echo $fileinfo->getPathname()." is ignored \n";
continue 2;
}
}
// gather info (sha1 and size) and add it to the file array
$files[$fileinfo->getPathname()] = array(
'sha1' => sha1_file($fileinfo->getPathname()),
'size' => $fileinfo->getSize()
);
}
// encode list to string and sign it
$result = array(
'ignoredPath' => $skippedPaths,
'files' => $files
);
$list = json_encode($result, JSON_PRETTY_PRINT);
$signed = $gpg->sign($list);
//write to file
$file = new \SplFileObject($directory.'/composer.sha1.json', 'w');
$written = $file->fwrite($signed);
$nbFiles = count($files);
echo <<<EOF
File $directory/composer.sha1.json file written, $nbFiles files found
EOF;
#!/usr/bin/php
<?php
$scriptName = $argv[0];
$helpText = <<<EOF
This script check the file integrity with local 'composer.sha1.json' file.
Usage: php $scriptName <expectedFingerprint>
-h --help Show this help
EOF;
// print help if required
if (in_array('-h', $argv) OR in_array('--help', $argv) OR count($argv) === 1) {
echo $helpText;
die();
}
// collect arguments
$directory = '.';
$expectedFingerprint = $argv[1];
//prepare array
$file = new \SplFileObject($directory.'/composer.sha1.json', 'r');
$clearSigned = $file->fread($file->getSize());
//prepare gpg
$gpg = new gnupg();
//verify signature
$text = "";
$info = $gpg->verify($clearSigned, false, $text);
if ($info !== FALSE) {
$keyInfos = $gpg->keyinfo($info[0]['fingerprint']);
//check the key can sign, is not revoked, disabled or expired
$filteredKeys = array_filter($keyInfos, function($key) {
return ($key['disabled'] == 0 && $key['expired'] == 0 && $key['can_sign'] == true
&& $key['revoked'] == 0);
});
if (count($filteredKeys) === 0) {
echo <<<EOF
The file composer.sha1.json signature is properly signed, but the key is revoked, has expired, is disabled or cannot sign.
EOF;
}
// check the signature used in composer.sha1.json is the expected one
if (preg_match('/[A-F0-9]*('.$expectedFingerprint.')$/', $info[0]['fingerprint']) !== 1) {
echo <<<EOF
The file composer.sha1.json is not signed by the expected key. The expected key was $expectedFingerprint. The signature is done using {$info[0]['fingerprint']}.
EOF;
die();
}
$keyAuthor = isset($filteredKeys[0]['uids'][0]['name']) ? $filteredKeys[0]['uids'][0]['name'] : 'not set';
$date = (new \DateTime())->setTimeStamp($info[0]['timestamp'])->format('c');
echo <<<EOF
The file $directory/composer.sha1.json is properly signed by $keyAuthor on $date.
EOF;
} else {
echo <<<EOF
The signing verification errored !
EOF;
}
$data = json_decode($text, true);
// prepare path
$signedFiles = $data['files'];
$skippedPaths = $data['ignoredPath'];
// print info about ignored path
echo "Those path are ignored by the signature and won't be verified : \n";
foreach ($skippedPaths as $path) {
echo "$path \n";
}
// prepare iteration on files
$directoryIterator = new \RecursiveDirectoryIterator($directory, FilesystemIterator::CURRENT_AS_FILEINFO);
$recursiveIterator = new \RecursiveIteratorIterator($directoryIterator);
// iterate on file
$erroredFiles = array();
foreach ($recursiveIterator as $fileinfo) {
// we skip ., .., and composer.sha1.json
if(in_array($fileinfo->getFilename(), array('.', '..', 'composer.sha1.json'))) {
continue;
}
//skip links -- Should we do this ?
if ($fileinfo->isLink()) {
continue;
}
// skip directory
foreach ($skippedPaths as $pattern) {
if (preg_match($pattern, $fileinfo->getPathname()) === 1) {
continue 2;
}
}
// this will help you save some code...
$filename = $fileinfo->getPathname();
//check the composer.sha1.json has a sha1 for this file
if (!isset($signedFiles[$filename])) {
$erroredFiles[$filename] = "The file was not present in the original package";
}
//check sha1 and length are equals
if (sha1_file($filename) === $signedFiles[$filename]['sha1'] && $fileinfo->getSize() === $signedFiles[$filename]['size']) {
// remove correct file from the array signedFile
unset($signedFiles[$filename]);
continue;
} else {
$erroredFiles[$filename] = "The file seems to be corrupted";
}
}
// verify signed file absent during verification
if (count($signedFiles) > 0) {
foreach ($signedFiles as $absentFile => $data) {
$erroredFiles[$absentFile] = "The file was present during signature, but is absent during verification";
}
}
// show result
if (count($erroredFiles) > 0) {
echo "Package verification failed! \n";
echo "Errors : \n";
foreach ($erroredFiles as $file => $error) {
echo "$file => $error\n";
}
} else {
echo "Yipiie! Package verification success !\n";
}
@bishopb
Copy link

bishopb commented Sep 21, 2015

+1 Aside, in a full implementation, rather than black listing paths we maybe should require the paths on which to base signature (maybe could be specified in or deduced from composer.json).

@bishopb
Copy link

bishopb commented Sep 22, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment