Files
2026-11-05 21:22:03 +00:00

157 lines
11 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft die Absicherung des System-Updates: Signatur mit tools/release-sign.php erzeugen und mit DeskHub prüfen,
* manipulierte oder falsch zugeordnete Pakete ablehnen, Release-Anhänge finden.
*/
putenv('DESKHUB_SQLITE_PATH=:memory:');
putenv('DESKHUB_TESTING=1');
$tmp = sys_get_temp_dir() . '/deskhub-upd-' . bin2hex(random_bytes(4));
mkdir($tmp);
// Schlüsselpaar erzeugen (Windows braucht eine minimale openssl-Konfiguration)
$cnf = $tmp . '/cnf';
file_put_contents($cnf, "[req]\ndistinguished_name=req_dn\n[req_dn]\n");
$k = openssl_pkey_new(['config' => $cnf, 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
openssl_pkey_export($k, $priv, null, ['config' => $cnf]);
$pub = openssl_pkey_get_details($k)['key'];
file_put_contents($tmp . '/privat.pem', $priv);
// Fremder Schlüssel (Angreifer)
$k2 = openssl_pkey_new(['config' => $cnf, 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
openssl_pkey_export($k2, $priv2, null, ['config' => $cnf]);
file_put_contents($tmp . '/fremd.pem', $priv2);
// Katalog-Vorgaben mit Herausgeber-Schlüssel für diesen Test
file_put_contents($tmp . '/defaults.php', "<?php return ['plugins_url' => 'https://plugins.example.test/catalog.php?type=plugins', 'themes_url' => 'https://plugins.example.test/catalog.php?type=themes', 'access_key' => '', 'publisher_key' => " . var_export($pub, true) . "];");
putenv('DESKHUB_CATALOG_DEFAULTS=' . $tmp . '/defaults.php');
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void {
if (!$ok) $failures[] = $what;
};
$sign = function (string $zip, string $key, string $version): array {
exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(dirname(__DIR__) . '/tools/release-sign.php') . ' ' . escapeshellarg($zip) . ' ' . escapeshellarg($key) . ' ' . escapeshellarg($version) . ' 2>&1', $out, $code);
return [$code, implode("\n", $out)];
};
$check(update_signature_required(), 'Mit eingebautem Herausgeber-Schlüssel wird eine Signatur verlangt');
$zip = $tmp . '/deskhub-1.0.7.zip';
$z = new ZipArchive(); $z->open($zip, ZipArchive::CREATE); $z->addFromString('version.php', '<?php // neu'); $z->close();
[$code, $out] = $sign($zip, $tmp . '/privat.pem', '1.0.7');
$check($code === 0 && is_file($zip . '.sig'), 'Signier-Werkzeug erzeugt die .sig-Datei: ' . $out);
$sig = (string) @file_get_contents($zip . '.sig');
$check(update_verify_package('1.0.7', $zip, $sig) === null, 'Gültige Signatur wird akzeptiert');
foreach (['docs/DeskHub-Handbuch.pdf' => false, 'docs/video/demo.mp4' => true, 'docs/anderes.pdf' => true, 'plugins/x/plugin.php' => true, 'uploads/a.png' => true, 'backups/b.zip' => true,
'app_downloads/DeskHub.apk' => true, 'tools/x.php' => true, 'tests/x.php' => true, 'staff/help.php' => false, 'includes/mailer.php' => false, 'assets/js/passkey.js' => false, 'docs/DeskHub-Handbuch.pdf.bak' => true, 'config/config.php' => true, 'Config/Config.php' => true, 'config/config.example.php' => true, 'config/.htaccess' => true, './config/config.php' => true, '.env' => true, 'configure.php' => false, 'tools/fix-permissions.sh' => false, 'tools/release-sign.php' => true] as $path => $skipped) {
$check(update_path_is_skipped($path) === $skipped, 'Update-Pfad ' . $path . ($skipped ? ' wird übersprungen' : ' wird installiert'));
}
$check(update_verify_package('v1.0.7', $zip, $sig) === null, 'Version mit führendem "v" passt ebenfalls');
$check(update_verify_package('1.0.8', $zip, $sig) !== null, 'Signatur einer anderen Version wird abgelehnt (kein Herunterstufen/Umetikettieren)');
$check(update_verify_package('1.0.7', $zip, '') !== null, 'Fehlende Signatur wird abgelehnt');
$check(update_verify_package('1.0.7', $zip, 'keinebase64signatur') !== null, 'Kaputte Signatur wird abgelehnt');
// Paket verändert
$evil = $tmp . '/evil.zip';
$z = new ZipArchive(); $z->open($evil, ZipArchive::CREATE); $z->addFromString('version.php', '<?php system($_GET["x"]);'); $z->close();
$check(update_verify_package('1.0.7', $evil, $sig) !== null, 'Verändertes Paket mit alter Signatur wird abgelehnt');
// Signatur mit fremdem Schlüssel
copy($zip, $tmp . '/zweit.zip');
[$code] = $sign($tmp . '/zweit.zip', $tmp . '/fremd.pem', '1.0.7');
$check($code === 0 && update_verify_package('1.0.7', $tmp . '/zweit.zip', (string) file_get_contents($tmp . '/zweit.zip.sig')) !== null, 'Signatur eines fremden Schlüssels wird abgelehnt');
// Plugin-Signatur darf nicht als Update-Signatur durchgehen (andere Nachricht)
openssl_sign(plugin_signature_message('deskhub', '1.0.7', hash_file('sha256', $zip)), $pluginSig, $k, OPENSSL_ALGO_SHA256);
$check(update_verify_package('1.0.7', $zip, base64_encode($pluginSig)) !== null, 'Eine Plugin-Signatur gilt nicht als Update-Signatur');
// Werkzeug: Eingabefehler
[$code] = $sign($zip, $tmp . '/privat.pem', 'kaputt');
$check($code !== 0, 'Ungültige Versionsnummer wird vom Werkzeug abgelehnt');
[$code] = $sign($tmp . '/gibt-es-nicht.zip', $tmp . '/privat.pem', '1.0.7');
$check($code !== 0, 'Fehlende ZIP wird vom Werkzeug abgelehnt');
// Release-Anhänge finden (Gitea-Format)
$release = ['assets' => [
['name' => 'deskhub-1.0.7.zip', 'browser_download_url' => 'https://git.example.test/a/deskhub-1.0.7.zip'],
['name' => 'deskhub-1.0.7.zip.sig', 'browser_download_url' => 'https://git.example.test/a/deskhub-1.0.7.zip.sig'],
['name' => 'deskhub-1.0.6.zip', 'browser_download_url' => 'https://git.example.test/a/deskhub-1.0.6.zip'],
]];
$a = update_find_assets($release, 'v1.0.7');
$check($a['zip'] === 'https://git.example.test/a/deskhub-1.0.7.zip' && $a['sig'] === 'https://git.example.test/a/deskhub-1.0.7.zip.sig', 'Anhänge der richtigen Version werden gefunden');
$a = update_find_assets(['assets' => [['name' => 'deskhub-1.0.7.zip', 'browser_download_url' => 'http://unsicher.example/x.zip']]], '1.0.7');
$check($a['zip'] === null, 'Nur https-Adressen werden akzeptiert');
$a = update_find_assets(['assets' => []], '1.0.7');
$check($a['zip'] === null && $a['sig'] === null, 'Ohne Anhänge: nichts gefunden');
$a = update_find_assets($release, '1.0.9');
$check($a['zip'] === null, 'Anhänge anderer Versionen zählen nicht');
// Zugangstoken: nur aus der Konfiguration, nie eingebaut
$check(DESKHUB_GITEA_TOKEN === '', 'Im Programmcode steckt kein Zugangstoken mehr');
$check(update_http_headers() === [], 'Ohne konfiguriertes Token wird kein Authorization-Header gesendet');
$check(!str_contains((string) file_get_contents(dirname(__DIR__) . '/version.php'), 'openssl_decrypt'), 'version.php enthält keinen verschlüsselten Token mehr');
// Zugangstoken aus den Einstellungen (verschlüsselt gespeichert)
set_setting('update_token', encrypt_secret('tok_EINSTELLUNG_1234567890'));
$check(update_http_headers() === ['Authorization: token tok_EINSTELLUNG_1234567890'], 'gespeichertes Token wird als Authorization-Header gesendet');
$check(update_token_info()[1] === 'einstellungen', 'Quelle des Tokens: Einstellungen');
set_setting('update_token', '');
$check(update_http_headers() === [] && update_token_info() === ['', ''], 'ohne Token: kein Header, keine Quelle');
// Ohne Herausgeber-Schlüssel (reiner Eigenbetrieb) wird keine Signatur verlangt
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
$check(function_exists('plugin_publisher_key'), 'Hilfsfunktion vorhanden');
putenv('DESKHUB_CATALOG_DEFAULTS=' . $tmp . '/defaults.php'); // wieder mit offiziellem Katalog
// ── Updates von der Plugin-Webseite ──────────────────────────────────────────
$check(update_publisher_endpoint(['list' => 1]) === 'https://plugins.example.test/app-release.php?platform=server&list=1', 'Update-Adresse liegt neben dem Katalog: ' . update_publisher_endpoint(['list' => 1]));
$GLOBALS['deskhub_catalog_defaults'] = ['plugins_url' => '', 'themes_url' => '', 'publisher_key' => $pub];
$check(update_publisher_endpoint([]) === '' && update_publisher_releases() === [], 'Ohne offiziellen Katalog gibt es keine Update-Adresse');
unset($GLOBALS['deskhub_catalog_defaults']);
$pubZip = $tmp . '/pub.zip';
$z = new ZipArchive(); $z->open($pubZip, ZipArchive::CREATE); $z->addFromString('version.php', "<?php define('DESKHUB_VERSION', '1.0.9');"); $z->close();
$pubSha = hash_file('sha256', $pubZip);
openssl_sign(update_signature_message('1.0.9', $pubSha), $pubSigRaw, $k, OPENSSL_ALGO_SHA256);
$entry = ['platform' => 'server', 'version' => '1.0.9', 'sha256' => $pubSha, 'size' => filesize($pubZip), 'notes' => 'Neu', 'created_at' => '2026-11-05 10:00:00', 'signature' => base64_encode($pubSigRaw)];
$older = ['platform' => 'server', 'version' => '1.0.8', 'sha256' => str_repeat('a', 64), 'size' => 10, 'notes' => '', 'created_at' => '2026-10-01 10:00:00', 'signature' => 'x'];
$served = [$older, $entry];
$tamper = false;
$GLOBALS['deskhub_http_get'] = function (string $url, int $max, array $headers = []) use (&$served, &$tamper, $pubZip): array {
if (str_contains($url, 'list=1')) return [200, json_encode(['platform' => 'server', 'releases' => $served]), null];
if (str_contains($url, 'version=1.0.9') && str_contains($url, 'download=1')) return [200, ($tamper ? 'X' : '') . file_get_contents($pubZip), null];
return [404, null, 'HTTP 404'];
};
$rels = update_publisher_releases();
$check(count($rels) === 2 && $rels[0]['tag_name'] === '1.0.9' && $rels[0]['source'] === 'publisher' && $rels[0]['body'] === 'Neu', 'Plugin-Webseite: Versionen werden geladen, neueste zuerst');
$check(update_publisher_find('v1.0.9')['sha256'] === $pubSha && update_publisher_find('9.9.9') === null, 'Version wird mit oder ohne „v“ gefunden');
[$got, $err] = update_publisher_fetch($rels[0]);
$check($err === null && $got !== null && is_file($got), 'Gültiges Paket wird geladen und geprüft: ' . var_export($err, true));
if ($got) @unlink($got);
$bad = $rels[0]; $bad['signature'] = base64_encode('falsch');
[$got, $err] = update_publisher_fetch($bad);
$check($got === null && $err !== null, 'Falsche Signatur wird abgelehnt');
$bad = $rels[0]; $bad['signature'] = '';
[$got, $err] = update_publisher_fetch($bad);
$check($got === null && $err !== null, 'Fehlende Signatur wird abgelehnt (Seite ohne Signierschlüssel)');
$tamper = true;
[$got, $err] = update_publisher_fetch($rels[0]);
$check($got === null && str_contains((string) $err, 'Prüfsumme'), 'Verändertes Paket scheitert an der Prüfsumme');
$tamper = false;
$served = [['version' => '../../x', 'sha256' => $pubSha], ['version' => '2.0.0', 'sha256' => 'kurz'], 'kaputt'];
$check(update_publisher_releases() === [], 'Einträge mit ungültiger Version oder Prüfsumme werden ignoriert');
$GLOBALS['deskhub_http_get'] = fn(string $url, int $max, array $h = []): array => [500, null, 'HTTP 500'];
$check(update_publisher_releases() === [], 'Fällt die Plugin-Webseite aus, ist die Liste leer (dann greift Gitea)');
unset($GLOBALS['deskhub_http_get']);
// Aufräumen
foreach (glob($tmp . '/*') ?: [] as $f) @unlink($f);
@rmdir($tmp);
if ($failures) {
fwrite(STDERR, "FEHLER:\n- " . implode("\n- ", $failures) . "\n");
exit(1);
}
echo "Update security checks passed.\n";