Last active
June 23, 2022 08:12
-
-
Save debuss/2d54d93947280d063ec1caa4a0ffd2df to your computer and use it in GitHub Desktop.
Pega Hotfix Validator script in PHP
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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