247 lines
11 KiB
PHP
247 lines
11 KiB
PHP
<?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>';
|
||
}
|