mirror of
https://github.com/Combodo/iTop.git
synced 2026-10-21 07:49:01 +02:00
Compare commits
1 Commits
develop
...
issue/9239
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c982775c48 |
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Test\UnitTest\Module\ItopCoreUpdate;
|
||||||
|
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopDataTestCase;
|
||||||
|
|
||||||
|
class CoreUpdateAvailabilityTest extends ItopDataTestCase
|
||||||
|
{
|
||||||
|
public const USE_TRANSACTION = false;
|
||||||
|
|
||||||
|
private const AUTHENTICATION_PASSWORD = 'test-password-123';
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->BackupConfiguration();
|
||||||
|
$this->AddLoginModeAndSaveConfiguration('url');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFunctionalAdministratorCanAccessUpgradeWhenEnabled(): void
|
||||||
|
{
|
||||||
|
$this->SetUpgradeEnabled(true);
|
||||||
|
|
||||||
|
$sMenuPage = $this->CallAsFunctionalAdministrator();
|
||||||
|
$this->assertSame(200, $this->GetLastHttpStatus());
|
||||||
|
$this->assertStringContainsString('data-menu-id="iTopUpdate"', $sMenuPage);
|
||||||
|
|
||||||
|
$this->CallAsFunctionalAdministrator('core_update.select_update_file', ['maintenance' => 'true']);
|
||||||
|
$this->assertSame(200, $this->GetLastHttpStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpgradeMenuIsHiddenWhenDisabled(): void
|
||||||
|
{
|
||||||
|
$this->SetUpgradeEnabled(false);
|
||||||
|
|
||||||
|
$sMenuPage = $this->CallAsFunctionalAdministrator();
|
||||||
|
|
||||||
|
$this->assertSame(200, $this->GetLastHttpStatus());
|
||||||
|
$this->assertStringNotContainsString('data-menu-id="iTopUpdate"', $sMenuPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDirectUpgradePageAccessIsDeniedWhenDisabled(): void
|
||||||
|
{
|
||||||
|
$this->SetUpgradeEnabled(false);
|
||||||
|
|
||||||
|
$this->CallAsFunctionalAdministrator('core_update.select_update_file', ['maintenance' => 'true']);
|
||||||
|
|
||||||
|
$this->assertSame(403, $this->GetLastHttpStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUpgradeAjaxEndpointIsDeniedWhenDisabled(): void
|
||||||
|
{
|
||||||
|
$this->SetUpgradeEnabled(false);
|
||||||
|
|
||||||
|
$this->CallAsFunctionalAdministrator('core_update_ajax.can_update_core');
|
||||||
|
|
||||||
|
$this->assertSame(403, $this->GetLastHttpStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function SetUpgradeEnabled(bool $bEnabled): void
|
||||||
|
{
|
||||||
|
// Expected config parameter; it is not currently declared by itop-core-update.
|
||||||
|
$this->oiTopConfig->Set('itop-core-update.enabled', $bEnabled);
|
||||||
|
$this->SaveItopConfFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function CallAsFunctionalAdministrator(?string $sRoute = null, array $aParams = []): string
|
||||||
|
{
|
||||||
|
$sLogin = 'core-update-admin-'.bin2hex(random_bytes(6));
|
||||||
|
$this->CreateContactlessUser(
|
||||||
|
$sLogin,
|
||||||
|
self::$aURP_Profiles['Administrator'],
|
||||||
|
self::AUTHENTICATION_PASSWORD
|
||||||
|
);
|
||||||
|
|
||||||
|
$aQuery = $aParams;
|
||||||
|
if (null !== $sRoute) {
|
||||||
|
$aQuery['route'] = $sRoute;
|
||||||
|
}
|
||||||
|
$aQuery['auth_user'] = $sLogin;
|
||||||
|
$aQuery['auth_pwd'] = self::AUTHENTICATION_PASSWORD;
|
||||||
|
$sUri = 'pages/UI.php';
|
||||||
|
if ([] !== $aQuery) {
|
||||||
|
$sUri .= '?'.http_build_query($aQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->CallItopUri($sUri, [], [CURLOPT_POST => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function GetLastHttpStatus(): int
|
||||||
|
{
|
||||||
|
$this->assertNotNull($this->aLastCurlGetInfo);
|
||||||
|
$this->assertArrayHasKey('http_code', $this->aLastCurlGetInfo);
|
||||||
|
|
||||||
|
return $this->aLastCurlGetInfo['http_code'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Combodo\iTop\Test\UnitTest\Module\ItopCoreUpdate;
|
||||||
|
|
||||||
|
use Combodo\iTop\CoreUpdate\Service\CoreUpdater;
|
||||||
|
use Combodo\iTop\Test\UnitTest\ItopTestCase;
|
||||||
|
|
||||||
|
class SignatureValidationTest extends ItopTestCase
|
||||||
|
{
|
||||||
|
private const RESOURCES_DIRECTORY = __DIR__.'/resources/';
|
||||||
|
|
||||||
|
private string $sWorkDirectory = '';
|
||||||
|
private string $sPublicKey;
|
||||||
|
private string $sDestinationDirectory;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->RequireOnceItopFile('datamodels/2.x/itop-core-update/src/Service/CoreUpdater.php');
|
||||||
|
|
||||||
|
$this->sWorkDirectory = __DIR__.'/.signature-test-'.bin2hex(random_bytes(8));
|
||||||
|
mkdir($this->sWorkDirectory, 0700, true);
|
||||||
|
$this->sDestinationDirectory = $this->sWorkDirectory.'/installation';
|
||||||
|
mkdir($this->sDestinationDirectory, 0700);
|
||||||
|
|
||||||
|
$this->sPublicKey = file_get_contents(self::RESOURCES_DIRECTORY.'dummy-public.pem');
|
||||||
|
file_put_contents($this->sDestinationDirectory.'/installation-sentinel.txt', 'unchanged');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
static::RecurseRmdir($this->sWorkDirectory);
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testValidSignedPackageIsAccepted(): void
|
||||||
|
{
|
||||||
|
// Expected API: verify with an injected test public key before extracting to the supplied destination.
|
||||||
|
CoreUpdater::ValidateAndExtractDownloadedFile(
|
||||||
|
self::RESOURCES_DIRECTORY.'dummy-signed.zip',
|
||||||
|
$this->sPublicKey,
|
||||||
|
$this->sDestinationDirectory
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame(
|
||||||
|
file_get_contents(self::RESOURCES_DIRECTORY.'package/file1'),
|
||||||
|
file_get_contents($this->sDestinationDirectory.'/file1')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPackageIsRejectedBeforeExtractionWhenManifestWasAlteredAfterSigning(): void
|
||||||
|
{
|
||||||
|
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-altered-manifest.zip');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPackageIsRejectedBeforeExtractionWhenCoveredFileWasAltered(): void
|
||||||
|
{
|
||||||
|
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-altered-content.zip');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPackageWithInvalidSignatureIsRejectedBeforeExtraction(): void
|
||||||
|
{
|
||||||
|
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-badly-signed.zip');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPackageWithoutSignatureIsRejectedBeforeExtraction(): void
|
||||||
|
{
|
||||||
|
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-unsigned.zip');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPackageWithoutManifestIsRejectedBeforeExtraction(): void
|
||||||
|
{
|
||||||
|
$this->AssertRejectedBeforeExtraction(self::RESOURCES_DIRECTORY.'dummy-missing-manifest.zip');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function AssertRejectedBeforeExtraction(string $sArchivePath): void
|
||||||
|
{
|
||||||
|
$aInstallationBefore = $this->SnapshotDirectory($this->sDestinationDirectory);
|
||||||
|
$bRejected = false;
|
||||||
|
try {
|
||||||
|
CoreUpdater::ValidateAndExtractDownloadedFile(
|
||||||
|
$sArchivePath,
|
||||||
|
$this->sPublicKey,
|
||||||
|
$this->sDestinationDirectory
|
||||||
|
);
|
||||||
|
} catch (\UnexpectedValueException $e) {
|
||||||
|
$bRejected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertTrue($bRejected, 'The invalid update package was not rejected.');
|
||||||
|
$this->assertSame($aInstallationBefore, $this->SnapshotDirectory($this->sDestinationDirectory));
|
||||||
|
$this->assertFileDoesNotExist($this->sDestinationDirectory.'/file1');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function SnapshotDirectory(string $sDirectory): array
|
||||||
|
{
|
||||||
|
$aSnapshot = [];
|
||||||
|
$oIterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($sDirectory, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
foreach ($oIterator as $oFile) {
|
||||||
|
if ($oFile->isFile()) {
|
||||||
|
$sRelativePath = substr($oFile->getPathname(), strlen($sDirectory) + 1);
|
||||||
|
$aSnapshot[$sRelativePath] = hash_file('sha256', $oFile->getPathname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ksort($aSnapshot);
|
||||||
|
|
||||||
|
return $aSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Usage: php build-package.php <content-dir> <private-key.pem> <output.zip>
|
||||||
|
|
||||||
|
[$script, $contentDir, $privateKeyPath, $outputZip] = $argv;
|
||||||
|
|
||||||
|
$contentDir = rtrim(realpath($contentDir), '/');
|
||||||
|
$manifestName = 'manifest.xml';
|
||||||
|
$signatureName = 'manifest.xml.sig'; // TODO: use the name the module expects
|
||||||
|
|
||||||
|
// 1. List files (excluding manifest and signature), sorted for a stable output
|
||||||
|
$files = [];
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($contentDir, FilesystemIterator::SKIP_DOTS)
|
||||||
|
);
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
$relativePath = substr($file->getPathname(), strlen($contentDir) + 1);
|
||||||
|
if (in_array($relativePath, [$manifestName, $signatureName], true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$files[] = $relativePath;
|
||||||
|
}
|
||||||
|
sort($files);
|
||||||
|
|
||||||
|
// 2. Build manifest.xml in your format
|
||||||
|
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<files>\n";
|
||||||
|
foreach ($files as $relativePath) {
|
||||||
|
$fullPath = "$contentDir/$relativePath";
|
||||||
|
$xml .= sprintf(
|
||||||
|
"<file><path>%s</path><md5>%s</md5><size>%d</size></file>\n",
|
||||||
|
htmlspecialchars($relativePath, ENT_XML1),
|
||||||
|
md5_file($fullPath),
|
||||||
|
filesize($fullPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$xml .= "</files>\n";
|
||||||
|
file_put_contents("$contentDir/$manifestName", $xml);
|
||||||
|
|
||||||
|
// 3. Sign the manifest
|
||||||
|
$privateKey = openssl_pkey_get_private(file_get_contents($privateKeyPath));
|
||||||
|
openssl_sign($xml, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||||
|
file_put_contents("$contentDir/$signatureName", $signature); // or base64_encode($signature)
|
||||||
|
|
||||||
|
// 4. Zip everything
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
$zip->open($outputZip, ZipArchive::CREATE | ZipArchive::OVERWRITE);
|
||||||
|
foreach (array_merge($files, [$manifestName, $signatureName]) as $relativePath) {
|
||||||
|
$zip->addFile("$contentDir/$relativePath", $relativePath);
|
||||||
|
}
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
echo "Built $outputZip with ".count($files)." files\n";
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDYyavpa+/92vWV
|
||||||
|
vm8VnqthJJ6J6cWZbeD/amvWQg3754Az66Gr4Lf0mTD+7iWbKzjcqANyDLOug3+J
|
||||||
|
Hpxaj0ogx04lBi+MqFZZfYydD1S/xjrNwF4UnE0KfSPF5ZqgDjhJqhtevWlo7lxq
|
||||||
|
KoI6902bmv1XVP8we+aUANQeCI0OI3VeMcIVZ76yEz7BDyrQ1t7Dj7H44gsR4MFQ
|
||||||
|
P4K5QAHJ6+zW36K8/oiIzasvQ5bTVgVyx4UIlBHbw5YRpNdieX+NUFHQnMDUiZ0y
|
||||||
|
1beJHujF24LUrmqAL95S3fADurxEsuDtTUow27z0ZLJ4vxmsGaGKYZT/9ZYJoHdu
|
||||||
|
ux91Xdf9AgMBAAECggEAKqsO5gNzmfBVs+JmdMflDVqqxZQ4aylgRoVIskQQAouf
|
||||||
|
BRkUmgLB0iewqAxxZGgOage7nd/4IZoKKQ/YUE6xlWAE9m6QP0wGqth2K5eP46r9
|
||||||
|
ee+p1L6qvyzANLwrQeNqmr0bY/s92wRHNjIlrbttD/picKTvETjhujGrjjm6oZRm
|
||||||
|
MwOvCJ+/NLOHQbSfmlVhEFQTwH+efCiEvegQYBeLDgBEx/K8K5W4KxbUSnbk0oAM
|
||||||
|
TFEqmuGpSr11uN25W1I2OEKF0Ow2heEv9n9tDgdg5JbiX67HeSpGbrKs7bD6MpAh
|
||||||
|
mrTEHKv6EC6q9RZEtJRnIHFRqMQd1HDzlGSdjebPFQKBgQD3+bG0522x0tU6qmXo
|
||||||
|
gPZMS9iQlCDFZ3q899y3Peu3+ThZAG3MFj1pmBbl2whjuC65EF6cW//O04jqGl6b
|
||||||
|
jNUwUgqcJMywes9JDQ2WWWQp81JAfyQAJ8gL5PHazi/ffK1B03Z9y47dQNy2UIAl
|
||||||
|
4BvhWS2CBYeoPsdYWFbnpHLgFwKBgQDfzZwNzv7qgkREuVNRTAcBxcnnVMVrndXp
|
||||||
|
3JFFDu3lt/JL+B0rxESwLkcgcEA3EUrY8y5Iq7NbnS6doVmZeCxi3sXbY/fEkyEy
|
||||||
|
PHVmAUzHChxcsupZrOWoIxQzNbP8kJB22v9ocKaV1FfBVkkJ2WzWYjcl+DscKZ6X
|
||||||
|
/EoZ4JbhCwKBgGhMiLcrRBMwN2lmdFiXrYCnwh4Ff8cn4BMNObOFAtktpMqiiIv3
|
||||||
|
qrr4MmFZgCzZMiW6weCIPY8ToBm/H53TXXPmffnw7HsqHHeAWv+q95tBcY4ZK944
|
||||||
|
3UZLdIJwiOg6O3FdbRA6Bwv+lRbFwzCrE270IFT42HcmeJgrB9qEa6DfAoGAJP9G
|
||||||
|
dvnrJjVWt82zOlNJLT/XOsjGwh2biUePNE1dgoZAJdLkb6HIgR+FXpVv6CqRpv+8
|
||||||
|
fQL1aYW4eRJ9T2OQYt/U8Ym7IeH4sOdRuQUp/hVrzt884oo0hPEXm8ea83uqhht5
|
||||||
|
8Xf98fLidvGerWQmG+KsnzhD/UncD+ADF/gKuTUCgYAWM5ulgrgdo/ceum1JzMsh
|
||||||
|
qRDoFN1ktXE8oZtoOvtWAECEzMk51QiuxCYikyE5jqg2Vys3LBZXqTvuyPzL/HDJ
|
||||||
|
oB734PY8hoI1BYrJ0dKjQCPfImX2VMJQZ+Yk/21UaBwOIdoGDC1NVLNBlvjvjEEv
|
||||||
|
IWp2vMPzX4CDAee+86uMjg==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Mmr6Wvv/dr1lb5vFZ6r
|
||||||
|
YSSeienFmW3g/2pr1kIN++eAM+uhq+C39Jkw/u4lmys43KgDcgyzroN/iR6cWo9K
|
||||||
|
IMdOJQYvjKhWWX2MnQ9Uv8Y6zcBeFJxNCn0jxeWaoA44SaobXr1paO5caiqCOvdN
|
||||||
|
m5r9V1T/MHvmlADUHgiNDiN1XjHCFWe+shM+wQ8q0Nbew4+x+OILEeDBUD+CuUAB
|
||||||
|
yevs1t+ivP6IiM2rL0OW01YFcseFCJQR28OWEaTXYnl/jVBR0JzA1ImdMtW3iR7o
|
||||||
|
xduC1K5qgC/eUt3wA7q8RLLg7U1KMNu89GSyeL8ZrBmhimGU//WWCaB3brsfdV3X
|
||||||
|
/QIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
that's a file 1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
nice content for file 1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
nicer content for file 2
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
another file 1
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<files>
|
||||||
|
<file><path>file1</path><md5>e7d0f0aa46a74bd08e0f0f268a19812b</md5><size>15</size></file>
|
||||||
|
<file><path>folder1/file1</path><md5>f0d7b0cc26724909f6cb17f55040e054</md5><size>23</size></file>
|
||||||
|
<file><path>folder1/file2</path><md5>1696bef05836b4787a86a6497781e161</md5><size>24</size></file>
|
||||||
|
<file><path>folder2/file1</path><md5>d9966d42983e134e3f5249a5f2be66f7</md5><size>14</size></file>
|
||||||
|
</files>
|
||||||
Binary file not shown.
Reference in New Issue
Block a user