From d9cddd5e28666dbee1b73ee0bd853e713f230e93 Mon Sep 17 00:00:00 2001 From: florianthepro Date: Tue, 4 Aug 2026 09:33:50 +0200 Subject: [PATCH] Create readme.md --- readme.md | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 readme.md diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..1756d6f --- /dev/null +++ b/readme.md @@ -0,0 +1,298 @@ +# Bürgerabstimmung + +Digitale Bürgerbeteiligung mit dem Personalausweis. Die gesamte Anwendung ist +**eine einzige Datei**: `index.php`. Kein Framework, kein Composer, keine +externen Dienste, keine Aufrufe zu Dritten. + +Konzept, Fachlogik und die ehrlichen Grenzen stehen im +[Whitepaper](whitepaper.md). + +> **Testbetrieb:** Im Auslieferungszustand läuft die Anwendung im Testmodus und +> zeigt auf jeder Seite ein Banner „Testbetrieb – keine offizielle Seite der +> Bundesregierung oder einer Behörde.“ Der Testmodus ist keine Variable im +> Quelltext, sondern ein Zustand der Installation, und wird über die Oberfläche +> beendet. + +--- + +## Installation + +1. `index.php` in das Webverzeichnis legen — Hauptverzeichnis oder Unterordner, + der Basispfad wird erkannt. +2. **PHP 8.0 oder neuer** mit `pdo_sqlite`, `mbstring` und `sodium`. Alle drei + gehören zur Standardausstattung jedes Hosters. +3. Seite aufrufen. Beim ersten Aufruf legt die Datei selbst an: + - `data/` — SQLite-Datenbank, Server-Geheimnis, Signaturschlüssel, + Protokolle und eine eigene `.htaccess`, die den Ordner sperrt + - `.htaccess` — saubere Adressen und Schutz interner Dateien + - `robots.txt` + +Mehr ist nicht nötig: **Apache und diese Datei.** + +Was in `data/` liegt oder im Betrieb dort entsteht (einiges erst bei Bedarf): + +| Datei | Inhalt | +|---|---| +| `buergerabstimmung.sqlite` | die Datenbank (Name aus der Entwicklung, ohne Bedeutung) | +| `secret.key` | Server-Geheimnis für die Stimmen-Marker — **niemals** herausgeben | +| `server_sign.key` | Signaturschlüssel für die profil.yaml | +| `authorized_keys.yaml` | freigegebene öffentliche Ausweis-Schlüssel | +| `config.yaml` | Einstellungen aus der Einrichtung (0600) | +| `setup.token` | Einrichtungsschlüssel, nach dem Umschalten gelöscht | +| `test_keys.list` | im Testbetrieb erzeugte Schlüssel, für das Aufräumen | +| `issued/` | private Schlüssel ausgegebener Demo-Ausweise | +| `app.log` | Sicherheitsprotokoll ohne Personenbezug | +| `php-error.log` | PHP-Fehlerprotokoll, sobald etwas anfällt | + +Der ganze Ordner ist über eine eigene `.htaccess` gesperrt und die Anwendung +beantwortet Anfragen darauf nicht — unter nginx ist die Sperre selbst zu setzen +(siehe unten). + +Meldet die Seite „Fast geschafft“, fehlen dem Verzeichnis Schreibrechte — +per FTP auf 755 oder 775 setzen und neu laden. + +**nginx statt Apache:** Ohne `.htaccess` arbeitet die Anwendung über automatisch +erzeugte `/index.php/…`-Adressen weiter. Für saubere Adressen eine gleichwertige +Regel setzen und **`/data/` serverseitig sperren**: + +```nginx +location /data/ { deny all; return 404; } +location / { try_files $uri /index.php$is_args$args; } +``` + +--- + +## Vom Testbetrieb in den Echtbetrieb + +Beim ersten Start ist der **Testmodus** aktiv. Die Anmeldeseite zeigt dann genau +einen Knopf („Test-Anmeldung starten“), der eine zufällige, als gültig +behandelte Sitzung erzeugt; Ausweis-Aufforderungen entfallen. So lässt sich die +Plattform vorführen, ohne einen Ausweis zu benötigen. + +Angemeldet steht oben rechts der Chip **„Testmodus“**. Er führt auf +`/setup` — **Echtbetrieb einrichten**: + +1. **Voraussetzungen prüfen** — Datenverzeichnis beschreibbar, HTTPS aktiv, + `sodium` vorhanden, Zugriffsschutz gesetzt, Anzahl freigegebener + Ausweis-Schlüssel. +2. **Zugang festlegen** — entweder **eigener eID-Server** (BSI TR-03130) oder + **eigene Trust-Liste**. Erfasst werden SOAP-Adresse, Client-Zertifikat und + -Schlüssel, die Aktivierungsadresse der Ausweis-App, die Abgleich-Adresse der + Freigabeliste und die Startadresse von Nect. Ein Knopf **prüft die + Verbindung**, bevor irgendetwas verändert wird. +3. **Umschalten** — erst bei gültigen Eingaben und passendem + **Einrichtungsschlüssel** werden die Einstellungen nach `data/config.yaml` + (Rechte 0600) geschrieben, alle Testdaten gelöscht und der Echtbetrieb + dauerhaft aktiviert. Die Verbindungsprüfung ist dabei nur auf dem + eID-Server-Weg zwingend; auf dem Weg über die eigene Trust-Liste wird kein + Server kontaktiert. + +Der **Einrichtungsschlüssel** steht in `data/setup.token` und ist nur über +Dateizugriff oder `php index.php setup-token` lesbar. Damit kann nur umschalten, +wer den Server betreibt — im Testbetrieb erzeugt sich sonst jeder per Knopfdruck +eine Sitzung. Nach dem Umschalten wird die Datei gelöscht und `/setup` ist zu. + +Beim Umschalten werden nur die **im Testmodus erzeugten** Schlüssel aus der +Freigabeliste entfernt; ausgegebene oder abgeglichene Ausweise bleiben erhalten. + +Danach zeigt die Anmeldeseite die Ausweis-Apps. **Eine Einschränkung, die man +kennen muss:** Auf dem Weg über die eigene Trust-Liste (`eid_mode = demo`) +erscheint zusätzlich der Knopf „Ausweis auflegen“, sobald in der Sitzung bereits +ein freigegebener Schlüssel liegt — etwa nach dem Öffnen eines +`/claim/`-Links aus `php index.php issue-card`. Dieser Weg ist für die +Ausgabe von Demo- und Pilot-Ausweisen gedacht; wer ihn nicht will, setzt beim +Umschalten `eid_mode = eid`. + +--- + +## Anmeldung mit dem Ausweis + +- **AusweisApp direkt eingebunden (BSI TR-03124).** Der Knopf leitet den Browser + auf die Aktivierungsadresse des eID-Clients + (`http://127.0.0.1:24727/eID-Client?tcTokenURL=…`). Die AusweisApp fängt diese + Adresse ab — am PC wie am Smartphone — und holt das **tcToken** unter + `/eid/tctoken`. Dieser Teil braucht **keine Konfiguration**. + Weil die App das Token als eigener HTTP-Client **ohne Browser-Cookie** holt, + verbindet ein Einmal-Nonce in der tcTokenURL beide Seiten (10 Minuten gültig). +- **Ohne eingerichteten eID-Server** liefert das tcToken bewusst nur eine + `CommunicationErrorAddress`: Die App bricht sauber ab, der Browser kommt + zurück, **angemeldet wird niemand**. +- **Wichtig:** Der Rücksprung `/eid/callback` ist in dieser Fassung noch **nicht + ausgeprägt** — er weist grundsätzlich ab. Über die Ausweis-Apps kommt daher + aktuell **niemand hinein**, auch mit eingerichtetem eID-Server nicht. Was + fehlt, ist die Prüfung der signierten Zusicherung des eID-Servers und die + Entnahme der geprüften Kennung. **Nect Wallet** ist als zweiter Anbieter + konfigurierbar und endet an derselben Stelle. +- **Nur freigegebene Ausweise.** Anmelden kann sich nur, wessen öffentlicher + Schlüssel in `data/authorized_keys.yaml` steht **und** wer den passenden + privaten Schlüssel besitzt (Signatur-Challenge). Die Identität ist der + öffentliche Schlüssel selbst — kein abgeleitetes Pseudonym. +- **Zeitfenster:** Der Anmeldenachweis gilt nur kurz — 5-Minuten-Fenster, + höchstens zwei; danach ist erneutes Auflegen nötig. **Im Testbetrieb ist diese + Prüfung abgeschaltet**, dort greifen nur die Sitzungs-Timeouts (30 Minuten + Leerlauf, 8 Stunden absolut). + +> **Ehrlich zur eID:** In Deutschland gibt es **keine** staatliche Liste aller +> Ausweis-Schlüssel und keine API, die sie liefert. Echte Prüfung läuft über die +> BSI-Zertifikatskette (TR-03110) in einem eID-Server, und ein Ausweis-Chip ist +> nur mit der AusweisApp und PIN lesbar — kein Browser kann das. Die Freigabeliste +> ist der Platzhalter für genau diese Vertrauensprüfung, `sync-keys` der +> Anschlusspunkt für eine eigene Trust-Liste. Kein Griff in ein Behördenregister. + +--- + +## Was die Seite kann + +- **Eine Seite.** Thema einbringen und Suche öffnen je ein eigenes Fenster; + darunter Merkzettel-Chips, die Gruppe „kürzlich abgestimmt (noch änderbar)“ + und die Themenliste. `/` , `/topics` und `/me` führen auf dieselbe Seite. + In der Kopfzeile steht rechts „Abmelden“, im Testbetrieb zusätzlich der Chip + „Testmodus“ und bei anstehender Jury-Aufgabe ein Hinweis darauf. +- **Ein Thema pro Tag** je Ausweis; das nächste ab Mitternacht. +- **Geltungsbereich** ohne Freitext. Mit JavaScript in drei Stufen + (Ebene → Land → Kreis); ohne JavaScript bleibt die einfache Auswahlliste mit + allen rund 400 Kreisen stehen. +- **Ende der Abstimmung — Datum, Zielwert oder beides.** Zwei Haken: „an einem + Datum“ und „bei erreichter Stimmenzahl“ (Einheit *X Stimmen* oder + *% Stimmen*). Beides zusammen ist erlaubt — es endet, was zuerst eintritt. +- **Abstimmen dafür/dagegen**, neutral heißt nicht abstimmen. Die eigene Stimme + ist 24 Stunden änderbar, danach fest. Der Zustand steht in den Knöpfen: + beide grau, die gewählte Seite blau solange änderbar, danach blass-blau + gegen abgedunkelt und nicht mehr bedienbar. +- **Zahlen aktualisieren sich ohne Neuladen** — ein schlanker JSON-Endpunkt, + alle zwölf Sekunden und beim Zurückkehren auf den Tab. Ohne JavaScript bleibt + der gerenderte Stand stehen. +- **Themen werden nie gelöscht.** Ein Thema ohne Stimmen kann der Verfasser + **archivieren**: es verschwindet aus Listen und Suche, bleibt aber unter + seiner Adresse erhalten. Ab der ersten Stimme entfällt auch das. +- **Gleiche Titel sind erlaubt, aber sichtbar** — das Formular zeigt beim Tippen + ähnliche Themen, die Themenseite listet sie unter „Ähnliche Themen“. +- **Drei Symbole oben rechts am Thema**, auf einer Höhe mit den Schildern für + Kategorie und Gebiet: + - **Lesezeichen** — merken: dieses Thema, seine Kategorie, sein Gebiet + - **Archivbox** — archivieren; nur für den Verfasser und nur solange noch + niemand abgestimmt hat + - **rote Flagge** — melden; nicht für den Verfasser und nicht bei beendeten + Themen. Läuft bereits eine Meldung, steht die Flagge unterlegt und ohne + Funktion da. +- **Themen lassen sich nicht bearbeiten.** Was eingebracht ist, bleibt wie es + ist — eine stille Umdeutung nach bereits abgegebenen Stimmen ist damit + ausgeschlossen. Wer sich vertan hat, archiviert (solange noch keine Stimme da + ist) und bringt das Thema neu ein. +- **Ohne JavaScript** funktionieren Anmeldung, Themen, Abstimmen, Melden und + Jury vollständig. JavaScript ergänzt Countdown, NFC-Auslösung, die stufige + Bereichsauswahl, die Live-Zahlen und den Duplikat-Hinweis — und ist für die + Selbstauskunft `profil.yaml` derzeit **erforderlich**. +- **Melden nur bei Gesetzesverstoß.** Ein Suchfeld führt zum eingebauten + Gesetzesregister, der gewählte Paragraph wird **1:1 zitiert** und der Jury + wortgleich vorgelegt. +- **Bürger-Jury:** 1 % der Nutzerschaft wird per Los gezogen (mindestens 5), + Quorum 0,5 % (mindestens 3), 24 Stunden Frist, 3 Tage Karenz. Melder, Verfasser + und laufende Jurorinnen sind ausgeschlossen. +- **Deutsch und Englisch**, gewählt beim Sitzungsbeginn, nirgends gespeichert. +- **Hell und dunkel** folgen der Systemeinstellung. + +--- + +## Adressen + +| Pfad | Zweck | +|---|---| +| `/start` | Sprachwahl beim Sitzungsbeginn (erster Aufruf landet hier) | +| `/auth` | Anmeldung mit dem Ausweis | +| `/` , `/topics` , `/me` | die Hauptseite | +| `/topic/{id}` | ein Thema | +| `/topic/{id}/archive` | archivieren (nur Verfasser, nur ohne Stimmen) | +| `/vote` , `/favorite` | Stimme, Merkzettel (POST) | +| `/report/{id}` , `/report` | melden | +| `/jury` , `/jury/vote` | Jury-Aufgabe | +| `/setup` , `/setup/check` , `/setup/finish` | Echtbetrieb einrichten (nur im Testbetrieb) | +| `/eid/start` , `/eid/tctoken` , `/eid/callback` | Ausweis-App | +| `/claim/{handle}` | Demo-Ausweis in die Sitzung laden | +| `/tap` , `/logout` , `/lang` | anmelden, abmelden, Sprache (POST) | +| `/profil.yaml` | verschlüsselte Selbstauskunft | +| `/server.pub` | öffentlicher Prüfschlüssel für die Selbstauskunft | +| `/api/topics` , `/api/similar` | JSON für Live-Zahlen und Duplikat-Hinweis | +| `/imprint` , `/privacy` | Impressum, Datenschutz | +| `/a/app.css` , `/a/app.js` , `/a/icon.svg` | Oberfläche | +| `/favicon.ico` , `/favicon.png` , `/apple-touch-icon.png` | Icons | +| `/robots.txt` | verbietet die Indexierung | +| `/data/…` | wird immer mit 404 beantwortet | + +--- + +## Kommandozeile + +```bash +php index.php selftest # 122 automatisierte Prüfungen der Fachregeln +php index.php cron # Wartungslauf (sonst beiläufig bei Seitenaufrufen) +php index.php seed 400 # Demo-Stimmen, anonym wie im Echtbetrieb +php index.php jurysim # Demo-Jury stimmt in laufenden Prüfungen ab +php index.php issue-card 3 # autorisierte Demo-Ausweise samt Abhol-Verweis +php index.php sync-keys # Freigabeliste aus der konfigurierten Trust-Liste +php index.php setup-token # Einrichtungsschlüssel für die Umschaltung +php index.php config # wirksame Einstellungen aus data/config.yaml + +php -S 127.0.0.1:8080 index.php # lokale Vorführung ohne Webserver +``` + +Für den Wartungslauf genügt eine Cron-Zeile; ohne sie läuft er beiläufig mit: + +```cron +*/5 * * * * php /var/www/html/index.php cron >/dev/null 2>&1 +``` + +--- + +## Sicherheit + +- Ausschließlich Prepared Statements, durchgängiges Output-Escaping +- CSP `default-src 'none'` ohne `unsafe-inline`; CSS, JavaScript und Icons + liefert die Datei selbst als eigene Routen aus. Keine Inhalte von Dritten, + keine Tracker, keine CDN +- Einmal-Token für **jede** POST-Anfrage — deckt CSRF ab und verhindert Replay +- Jede Änderung läuft über einen **versiegelten, zeitgebundenen Umschlag**: die + Karte versiegelt die Aktion, der Server öffnet mit dem öffentlichen Schlüssel +- **Stimmen ohne Ausweis-Bezug gespeichert** — nur ein HMAC aus Thema und + öffentlichem Schlüssel mit Server-Geheimnis. Ohne dieses Geheimnis lässt sich + nicht rückschließen, welcher Ausweis wie gestimmt hat; Doppelstimmen bleiben + ausgeschlossen +- **profil.yaml** wird bei jedem Seitenaufruf frisch angefordert, an den + öffentlichen Ausweis-Schlüssel verschlüsselt und servergegengezeichnet + (Prüfschlüssel unter `/server.pub`); „Abmelden“ löscht sie +- Sitzungen: HttpOnly, SameSite, ID-Rotation, Idle- und Absolut-Timeout +- Kernregeln zusätzlich als DB-Constraints: 1 Thema/Tag, 1 Stimme/Thema, + 1 offene Meldung/Thema, 1 Jury-Sitz/Meldung +- Jury-Losverfahren mit CSPRNG, Ratenbegrenzung ohne Klar-IP-Speicherung +- Keine Klaridentitäten, keine Passwörter, keine externen Abhängigkeiten + +--- + +## Vor einem echten Betrieb + +- **Die Datenschutzerklärung sagt derzeit drei Dinge, die nicht stimmen.** + `/privacy` behauptet (a) es werde „ausschließlich ein Pseudonym (Hash mit + serverseitigem Geheimnis)“ gespeichert — tatsächlich ist die Kennung der + öffentliche Ausweis-Schlüssel selbst; (b) es werde „nichts im Browser + gespeichert“ — die `profil.yaml` liegt im `sessionStorage`; (c) das Konto + lasse sich „jederzeit in ‚Meine Übersicht‘ löschen“ — diese Seite und dieser + Knopf existieren nicht. **Das ist vor jedem öffentlichen Betrieb zu + korrigieren**, es sind Falschangaben gegenüber Nutzern. +- **Impressum füllen.** `/imprint` enthält nur einen Platzhalter (`*`) und ist + mit den Pflichtangaben des Betreibers zu ersetzen. +- **Selbstauskunft ohne JavaScript ermöglichen** und einen Weg zur + **Konto-Löschung in der Oberfläche** ergänzen — die Routine `account_delete` + existiert, hat aber keine Schaltfläche. Ohne beides ist die DSGVO nicht + erfüllt. +- **Den Rücksprung `/eid/callback` ausprägen** — ohne ihn führt kein + Ausweis-App-Weg zu einer Anmeldung. +- **`data/` außerhalb des Webroots legen.** Datenbank und Geheimnisse liegen + derzeit im Webroot und sind nur durch `.htaccess` und eine 404-Regel der + Anwendung geschützt. +- **Gesetzestexte** wortgleich gegen gesetze-im-internet.de abgleichen. +- **Gebietsverzeichnis** (16 Länder, rund 400 Kreise) gegen das amtliche + Verzeichnis (ARS/Destatis) abgleichen. +- **Externes Sicherheits-Audit und Pentest**, Datenschutz-Folgenabschätzung, + Barrierefreiheit nach BITV. +- Die Seite ist **kein Wahlsystem** und darf nicht als solches bezeichnet + werden. Sie hat keine Rechtsverbindlichkeit.