'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 '

Signatur entfernt.

'; } echo '

AeroMC Update-Signaturen

'; echo '

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.

'; if (!function_exists('sodium_crypto_sign_verify_detached')) { echo '

PHP hat kein libsodium – hochgeladene Signaturen werden hier nicht gegengeprüft ' . '(der Launcher prüft sie trotzdem selbst).

'; } $alle = aeromc_sig_alle(); if (!$alle) { echo '

Noch keine Signaturen vorhanden.

'; return; } echo ''; foreach ($alle as $schl => $s) { echo '' . ''; } echo '
DateiVersionSHA-256Hochgeladen
' . esc_html($s['datei']) . '' . esc_html($s['version']) . '' . esc_html(substr($s['sha256'], 0, 16)) . '…' . esc_html($s['hochgeladen']) . '' . '
'; wp_nonce_field('aeromc_signaturen'); echo '' . '
'; }