$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', " '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', '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', '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', "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";