Files
2026-10-07 07:32:04 +00:00

247 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Update-Signaturen für den AeroMC Launcher
* -----------------------------------------
* Der Launcher installiert ein Update nur, wenn es mit dem Release-Schlüssel des
* Entwicklers unterschrieben ist (Ed25519). Die Unterschriften liegen hier statt
* im Gitea-Release; die EXE selbst kommt weiter aus Gitea.
*
* GET /wp-json/aeromc/v1/update-signatur?datei=AeroMC-Setup.exe&version=1.2.2
* öffentlich – liefert die Unterschrift (sie ist nicht geheim)
* POST /wp-json/aeromc/v1/update-signatur
* nur Administratoren (Anwendungspasswort) – vom Build-Skript
* scripts/sign-release.js automatisch aufgerufen
* GET /wp-json/aeromc/v1/update-signatur/zugang
* prüft beim Einrichten, ob der Zugang stimmt, und sagt sonst, woran es scheitert
*
* Wichtig: Der geheime Schlüssel liegt NIE auf diesem Server. Hier wird nur
* mit dem öffentlichen Schlüssel geprüft, dass eine hochgeladene Unterschrift
* echt ist – selbst ein übernommener Admin-Zugang kann keine gültige erzeugen.
*/
if (!defined('ABSPATH')) {
exit;
}
define('AEROMC_SIG_OPTION', 'aeromc_update_signaturen');
define('AEROMC_SIG_FORMAT', 'AeroMC-Update/1');
// öffentlicher Release-Schlüssel (identisch mit src/update-schluessel.pem im Launcher)
define('AEROMC_SIG_SCHLUESSEL', 'MCowBQYDK2VwAyEAXxt1cCntDQzvej4hjyVZt6Onj6Uv3hSlneJaOxxmsds=');
define('AEROMC_SIG_DATEIEN', ['AeroMC-Setup.exe', 'AeroMC-Portable.exe']);
define('AEROMC_SIG_MAX', 40); // so viele Einträge werden aufgehoben (älteste fliegen raus)
add_action('rest_api_init', function () {
register_rest_route('aeromc/v1', '/update-signatur', [
[
'methods' => 'GET',
'callback' => 'aeromc_sig_route_get',
'permission_callback' => '__return_true',
],
[
'methods' => 'POST',
'callback' => 'aeromc_sig_route_post',
'permission_callback' => 'aeromc_sig_ist_admin',
],
]);
// Zugangsprüfung mit Diagnose: sagt dem Build-Skript genau, woran eine
// Anmeldung scheitert (Header kommt nicht an, falsches Passwort, Rolle fehlt …)
register_rest_route('aeromc/v1', '/update-signatur/zugang', [
'methods' => 'GET',
'callback' => 'aeromc_sig_route_zugang',
'permission_callback' => '__return_true',
]);
});
/*
* Eigene Rolle "AeroMC Release": darf NUR Signaturen hochladen (sonst nichts
* außer dem eigenen Profil). Für das Build-Skript gedacht – so muss kein
* Administrator-Zugang auf dem Entwickler-PC liegen.
*/
define('AEROMC_SIG_RECHT', 'aeromc_signaturen_hochladen');
add_action('init', function () {
if (!get_role('aeromc_release')) {
add_role('aeromc_release', 'AeroMC Release', ['read' => true, AEROMC_SIG_RECHT => true]);
}
});
function aeromc_sig_route_zugang() {
$authorization = !empty($_SERVER['HTTP_AUTHORIZATION']) || !empty($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])
|| !empty($_SERVER['PHP_AUTH_USER']);
$eigener = !empty($_SERVER['HTTP_X_AEROMC_AUTH']);
$grund = '';
$user = is_user_logged_in() ? wp_get_current_user() : null;
if (!$user && $eigener) {
$roh = base64_decode((string) $_SERVER['HTTP_X_AEROMC_AUTH'], true);
if ($roh && strpos($roh, ':') !== false) {
list($benutzer, $passwort) = explode(':', $roh, 2);
$r = wp_authenticate_application_password(null, $benutzer, $passwort);
if ($r instanceof WP_User) {
$user = $r;
wp_set_current_user($r->ID);
} elseif (is_wp_error($r)) {
$grund = $r->get_error_code();
} else {
$grund = 'nicht_geprueft';
}
} else {
$grund = 'kopf_ungueltig';
}
}
$darf = $user && (user_can($user, AEROMC_SIG_RECHT) || user_can($user, 'manage_options'));
return new WP_REST_Response([
'ok' => (bool) $darf,
'version' => AEROMC_FRIENDS_VERSION,
'authorization_kommt_an' => $authorization,
'eigener_kopf_kommt_an' => $eigener,
'anwendungspasswoerter' => function_exists('wp_is_application_passwords_available') && wp_is_application_passwords_available(),
'benutzer' => $user ? $user->user_login : null,
'darf_hochladen' => (bool) $darf,
'grund' => $grund,
], $darf ? 200 : 401);
}
function aeromc_sig_ist_admin() {
aeromc_sig_anmelden_ueber_kopf();
return current_user_can(AEROMC_SIG_RECHT) || current_user_can('manage_options');
}
/*
* Manche Server (Apache mit FastCGI/PHP-FPM, Proxys) geben den Header
* "Authorization" nicht an PHP weiter – die normale Anmeldung per
* Anwendungspasswort kommt dann nie an. Das Build-Skript schickt dieselben
* Daten deshalb zusätzlich als "X-AeroMC-Auth: base64(benutzer:passwort)".
* Geprüft wird mit der WordPress-eigenen Funktion für Anwendungspasswörter –
* das normale Login-Passwort wird hier NICHT angenommen.
*/
function aeromc_sig_anmelden_ueber_kopf() {
if (is_user_logged_in()) {
return;
}
$kopf = isset($_SERVER['HTTP_X_AEROMC_AUTH']) ? (string) $_SERVER['HTTP_X_AEROMC_AUTH'] : '';
$roh = $kopf !== '' ? base64_decode($kopf, true) : false;
if (!$roh || strpos($roh, ':') === false) {
return;
}
list($benutzer, $passwort) = explode(':', $roh, 2);
$user = wp_authenticate_application_password(null, $benutzer, $passwort);
if ($user instanceof WP_User) {
wp_set_current_user($user->ID);
}
}
function aeromc_sig_version_norm($v) {
return preg_replace('/^v/i', '', trim((string) $v));
}
function aeromc_sig_schluessel_von($datei, $version) {
return $datei . '|' . aeromc_sig_version_norm($version);
}
function aeromc_sig_alle() {
$alle = get_option(AEROMC_SIG_OPTION, []);
return is_array($alle) ? $alle : [];
}
function aeromc_sig_route_get($request) {
$datei = (string) $request->get_param('datei');
$version = aeromc_sig_version_norm($request->get_param('version'));
$alle = aeromc_sig_alle();
$eintrag = $alle[aeromc_sig_schluessel_von($datei, $version)] ?? null;
if (!$eintrag) {
return new WP_REST_Response(['ok' => false, 'message' => 'Keine Signatur für diese Datei/Version.'], 404);
}
unset($eintrag['hochgeladen']);
$antwort = new WP_REST_Response($eintrag, 200);
$antwort->header('Cache-Control', 'no-store');
return $antwort;
}
/**
* Prüft Aufbau und – sofern PHP libsodium mitbringt – die Unterschrift selbst.
* Gibt null zurück, wenn alles stimmt, sonst die Fehlermeldung.
*/
function aeromc_sig_pruefe(array $s) {
if (($s['format'] ?? '') !== AEROMC_SIG_FORMAT) return 'Unbekanntes Format.';
if (!in_array($s['datei'] ?? '', AEROMC_SIG_DATEIEN, true)) return 'Unbekannter Dateiname.';
if (!preg_match('/^\d+\.\d+\.\d+(-[0-9A-Za-z.\-]+)?$/', aeromc_sig_version_norm($s['version'] ?? ''))) return 'Ungültige Version.';
if (!preg_match('/^[0-9a-f]{64}$/', (string) ($s['sha256'] ?? ''))) return 'Ungültige Prüfsumme.';
$unterschrift = base64_decode((string) ($s['signatur'] ?? ''), true);
if ($unterschrift === false || strlen($unterschrift) !== 64) return 'Ungültige Signatur.';
if (function_exists('sodium_crypto_sign_verify_detached')) {
$der = base64_decode(AEROMC_SIG_SCHLUESSEL);
$oeffentlich = substr($der, -32); // SPKI-Hülle (12 Byte) + 32 Byte Schlüssel
$text = implode("\n", [AEROMC_SIG_FORMAT, $s['datei'], aeromc_sig_version_norm($s['version']), $s['sha256']]);
if (!sodium_crypto_sign_verify_detached($unterschrift, $text, $oeffentlich)) {
return 'Signatur passt nicht zum Release-Schlüssel.';
}
}
return null;
}
function aeromc_sig_route_post($request) {
$s = $request->get_json_params();
if (!is_array($s)) {
return new WP_REST_Response(['ok' => false, 'message' => 'Kein JSON erhalten.'], 400);
}
$s = [
'format' => (string) ($s['format'] ?? ''),
'datei' => (string) ($s['datei'] ?? ''),
'version' => aeromc_sig_version_norm($s['version'] ?? ''),
'sha256' => strtolower((string) ($s['sha256'] ?? '')),
'signatur' => (string) ($s['signatur'] ?? ''),
];
$fehler = aeromc_sig_pruefe($s);
if ($fehler) {
return new WP_REST_Response(['ok' => false, 'message' => $fehler], 400);
}
$alle = aeromc_sig_alle();
$s['hochgeladen'] = current_time('mysql');
$alle[aeromc_sig_schluessel_von($s['datei'], $s['version'])] = $s;
// nur die neuesten Einträge behalten
uasort($alle, function ($a, $b) { return strcmp($b['hochgeladen'], $a['hochgeladen']); });
$alle = array_slice($alle, 0, AEROMC_SIG_MAX, true);
update_option(AEROMC_SIG_OPTION, $alle, false);
return ['ok' => true, 'datei' => $s['datei'], 'version' => $s['version'],
'geprueft' => function_exists('sodium_crypto_sign_verify_detached')];
}
// Übersicht im Admin-Menü (unter "AeroMC Fehler")
add_action('admin_menu', function () {
add_submenu_page('aeromc-reports', 'AeroMC Update-Signaturen', 'Update-Signaturen', 'manage_options',
'aeromc-signaturen', 'aeromc_sig_admin_seite');
}, 20);
function aeromc_sig_admin_seite() {
if (!current_user_can('manage_options')) {
return;
}
if (isset($_POST['aeromc_sig_loeschen']) && check_admin_referer('aeromc_signaturen')) {
$alle = aeromc_sig_alle();
unset($alle[(string) wp_unslash($_POST['aeromc_sig_loeschen'])]);
update_option(AEROMC_SIG_OPTION, $alle, false);
echo '<div class="notice notice-success"><p>Signatur entfernt.</p></div>';
}
echo '<div class="wrap"><h1>AeroMC Update-Signaturen</h1>';
echo '<p>Diese Unterschriften lädt das Build-Skript automatisch hoch. Der Launcher installiert ein Update nur, '
. 'wenn hier eine passende, gültige Signatur liegt. Sie sind nicht geheim – der geheime Schlüssel bleibt auf dem Entwickler-PC.</p>';
if (!function_exists('sodium_crypto_sign_verify_detached')) {
echo '<div class="notice notice-warning"><p>PHP hat kein libsodium – hochgeladene Signaturen werden hier nicht gegengeprüft '
. '(der Launcher prüft sie trotzdem selbst).</p></div>';
}
$alle = aeromc_sig_alle();
if (!$alle) {
echo '<p><em>Noch keine Signaturen vorhanden.</em></p></div>';
return;
}
echo '<table class="widefat striped"><thead><tr><th>Datei</th><th>Version</th><th>SHA-256</th><th>Hochgeladen</th><th></th></tr></thead><tbody>';
foreach ($alle as $schl => $s) {
echo '<tr><td>' . esc_html($s['datei']) . '</td><td>' . esc_html($s['version']) . '</td>'
. '<td><code>' . esc_html(substr($s['sha256'], 0, 16)) . '…</code></td><td>' . esc_html($s['hochgeladen']) . '</td><td>'
. '<form method="post" onsubmit="return confirm(\'Signatur wirklich entfernen? Dann bietet der Launcher dieses Update nicht mehr an.\');">';
wp_nonce_field('aeromc_signaturen');
echo '<input type="hidden" name="aeromc_sig_loeschen" value="' . esc_attr($schl) . '" />'
. '<button class="button button-link-delete">Entfernen</button></form></td></tr>';
}
echo '</tbody></table></div>';
}