Skip to content

Instantly share code, notes, and snippets.

@debuss
Last active June 23, 2022 08:12
Show Gist options
  • Save debuss/2d54d93947280d063ec1caa4a0ffd2df to your computer and use it in GitHub Desktop.
Save debuss/2d54d93947280d063ec1caa4a0ffd2df to your computer and use it in GitHub Desktop.
Pega Hotfix Validator script in PHP
<?php
/**
* Pega Hotfix Validator script.
*
* With this little script you can manually validate a hotfix download from Pega MPS.
* It simply process the different steps described in the Pega documentation, see below.
*
* @author Alexandre Debusschère <alexandre.debusschere@hey.com>
* @see https://docs.pega.com/keeping-current-pega/86/manually-verifying-hotfix-files-using-third-party-tools
*/
if (!version_compare(PHP_VERSION, '7.4', '>=')) {
echo 'You need at least PHP v7.4 to run this script.';
exit(1);
}
/**
* Deal with command lines to validate a hotfix.
*
* @version 1.0.0
*/
class HotfixValidator
{
private const EXIT_SUCCESS = 0;
private const EXIT_FAILURE = 1;
protected int $argc;
protected array $argv;
protected SplFileInfo $hotfix;
protected string $working_directory;
protected array $sig;
public function __construct(int $argc, array $argv)
{
$this->argc = $argc;
$this->argv = $argv;
}
protected function out(string|array $messages): void
{
foreach ((array)$messages as $message) {
echo ($message).PHP_EOL;
}
}
protected function success(string|array $messages): void
{
$this->out(array_map(fn($message) => "\e[0;32m".$message."\e[0m", (array)$messages));
}
protected function info(string|array $messages): void
{
$this->out(array_map(fn($message) => "\e[0;36m".$message."\e[0m", (array)$messages));
}
protected function error(string|array $messages): void
{
$this->out(array_map(fn($message) => "\e[0;31m".$message."\e[0m", (array)$messages));
exit(self::EXIT_FAILURE);
}
protected function help(): void
{
$this->out([
'Pega Hotfix Validator script',
'',
'Usage: php hf-validator.php [file.zip]',
'',
'Required argument:',
' file.zip',
' The hotfix zip file downloaded from Pega MSP'
]);
}
protected function extractHotfix(): void
{
$this->hotfix = new SplFileInfo($this->argv[1]);
if (!$this->hotfix->isFile() || strtolower($this->hotfix->getExtension()) != 'zip') {
$this->error('Provided hotfix is not a file or not ZIP file...');
}
$zip = new ZipArchive();
if ($zip->open($this->hotfix->getRealPath()) !== true) {
$this->error('Unable to unzip provided file...');
}
$this->working_directory = './'.$this->hotfix->getBasename('.zip');
$zip->extractTo($this->working_directory);
$zip->close();
$this->out('ZIP file extracted to '.$this->working_directory);
}
protected function setWorkingDirectory(): void
{
chdir($this->working_directory);
$this->out('Changed working directory to '.$this->working_directory);
}
protected function extractJsonSigfile(): void
{
$this->sig = json_decode(file_get_contents('SIGFILE.JSON'), true);
$this->out('Extracted and decoded SIGFILE.JSON');
}
protected function executeCommand(string $cmd, ?callable $interpreter = null): void
{
$output = null;
$returned_value = null;
exec($cmd, $output, $returned_value);
call_user_func(
$interpreter ?: function ($output, $returned_value) use ($cmd) {
if ($returned_value !== 0) {
$this->error('The command line failed: '.$cmd);
}
$this->out(['Succeeded:', ' > '.$cmd]);
},
$output,
$returned_value
);
}
protected function decodeCertificatesToFiles(): void
{
$pegasystems = $this->sig['certificates'][0];
$intermediate = $this->sig['certificates'][1];
$cmd = sprintf('echo %s | base64 --decode > pegasystems.der', $pegasystems);
$this->executeCommand($cmd);
$cmd = sprintf('echo %s | base64 --decode > intermediate.der', $intermediate);
$this->executeCommand($cmd);
}
protected function translateCertificatesIntoCrtFormat(): void
{
$cmd = 'openssl x509 -in pegasystems.der -inform der > pegasystems.crt';
$this->executeCommand($cmd);
$cmd = 'openssl x509 -in intermediate.der -inform der > intermediate.crt';
$this->executeCommand($cmd);
}
protected function verifySubjectCertificate(): void
{
$cmd = 'openssl x509 -in pegasystems.crt -text -noout';
$this->executeCommand($cmd, function ($output, $returned_value) use ($cmd) {
if ($returned_value !== 0) {
$this->error('The command line failed: '.$cmd);
}
$values = 'C = US, ST = Massachusetts, L = Cambridge, O = Pegasystems Inc., CN = Pegasystems Inc.';
if (!str_contains($output[10], $values)) {
$this->error('Unable to match the certificate subject with the required one.');
}
$this->out(['Succeeded:', ' > '.$cmd]);
});
}
protected function verifyCertificateChain(): void
{
$cmd = 'openssl verify -crl_download -crl_check -untrusted intermediate.crt pegasystems.crt';
$this->executeCommand($cmd, function ($output, $returned_value) use ($cmd) {
if ($returned_value !== 0) {
$this->error('The command line failed: '.$cmd);
}
if ($output[0] !== 'pegasystems.crt: OK') {
$this->error(sprintf(
'The command line "%s" did not return "pegasystems.crt: OK" but : %s',
$cmd,
$output[0]
));
}
$this->out(['Succeeded:', ' > '.$cmd]);
});
}
protected function extractPublicKeyFromPegaSystemsCertificate(): void
{
$cmd = 'openssl x509 -pubkey -noout -in pegasystems.der -inform der > pubkey.pub';
$this->executeCommand($cmd);
}
protected function checkSignatures(): void
{
foreach ($this->sig['signatures'] as ['path' => $path, 'signature' => $signature]) {
$this->info('Checking file: '.$path);
$cmd = sprintf('echo %s | base64 --decode > signature.sig', $signature);
$this->executeCommand($cmd);
$cmd = sprintf(
'openssl dgst -verify pubkey.pub -keyform PEM -sha256 -signature signature.sig %s',
$path
);
$this->executeCommand($cmd, function ($output, $returned_value) use ($cmd) {
if ($returned_value !== 0) {
$this->error('The command line failed: '.$cmd);
}
if ($output[0] !== 'Verified OK') {
$this->error(sprintf(
'The command line "%s" did not return "Verified OK": %s',
$cmd,
$output[0]
));
}
$this->out(['Succeeded:', ' > '.$cmd]);
});
}
}
protected function deleteExtractFolder(): void
{
chdir('..');
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($this->working_directory, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $filename => $file_info) {
$file_info->isDir() ?
rmdir($filename) :
unlink($filename);
}
rmdir($this->working_directory);
$this->out(sprintf('Cleaned folder %s', $this->working_directory));
}
public function run(): void
{
if ($this->argc < 2) {
$this->help();
$this->error('Missing hotfix ZIP file...');
}
$this->extractHotfix();
$this->setWorkingDirectory();
$this->extractJsonSigfile();
$this->decodeCertificatesToFiles();
$this->translateCertificatesIntoCrtFormat();
$this->verifySubjectCertificate();
$this->verifyCertificateChain();
$this->extractPublicKeyFromPegaSystemsCertificate();
$this->checkSignatures();
$this->deleteExtractFolder();
$this->success(sprintf('Hotfix %s validated successfully !', $this->hotfix->getBasename()));
exit(self::EXIT_SUCCESS);
}
}
$validator = new HotfixValidator($argc, $argv);
$validator->run();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment