docs: Die Lehren aus 2.9.3 in die Uebernahme-Anleitung

Was die Sicherheitsbegutachtung an Erkenntnis gebracht hat, galt bisher
nur zur Haelfte im Dokument: Abschnitt 12 nannte die Befunde, aber drei
Dinge fehlten, die im Zielprojekt genauso gelten.

NEU §13 - was welche Zertifizierungsanforderung erfuellt
Die Zahlen in diesem Dokument sind nicht gegriffen, sie stammen aus
Anforderungen: 30 Minuten und 12 Stunden aus AAL2, zwoelf Zeichen und
hoechstens zehn Fehlversuche aus PCI DSS 4.0, die Wiederanmeldung vor
Aenderungen an den Anmeldemitteln aus ASVS. Wer eine Zahl aendert, soll
wissen, welche Anforderung er dabei verlaesst. Mit zwei Ehrlichkeiten:
Eine Begutachtung der Authentifizierungskapitel kann auch nur ueber
diese eine Aussage treffen, und ISO zertifiziert das Managementsystem,
nicht den Code.

§13.1 nennt den Fall, in dem die Anforderungen sich widersprechen - 30
Minuten als VOLLE Neuanmeldung erzeugen genau das Verhalten, das der
Standard nicht will: Der Schluessel bleibt stecken.

NEU §14 - wie man belegt, was sich nicht unit-testen laesst
Drei der wichtigsten Zusicherungen sind mit einem gewoehnlichen
Testfall nicht pruefbar. Der negative Test muss den ABLEHNUNGSGRUND
pruefen, sonst bestaetigt er irgendwann nur noch, dass irgendetwas
kaputt ist. Das Zehn-Minuten-Fenster verlangt echtes Warten; es fuer
den Test zu verkleinern prueft eine andere Anlage. Und der Gegenfall
mit echter Hardware laesst sich gar nicht automatisieren - das ist ein
einzuplanender Handgriff, keine Luecke.

Samt der Falle, in die die eigene Probe zuerst gelaufen ist: zwei
Zustaende, die sich gegenseitig ausschliessen, durch dieselbe Sitzung
geschickt.

NEU §12.8 - ein Bildschirm fuer Leerlauf und Bestaetigung
Beide verlangen dasselbe. Zwei Stellen, die Passwoerter pruefen, laufen
auseinander. Der Leerlaufzustand laesst sich willentlich herbeifuehren,
und die ganze Wirkung haengt dann an einer Zeile in isLoggedIn() - jede
Stelle, die sie fragt, bekommt ohne eigenes Zutun die richtige Antwort.

GESCHAERFT §12.6
Die Ankerdiagnose muss VERGLEICHEN, nicht nur nachsehen, ob ein Wert
gesetzt ist: Ein Tippfehler weist den Schluessel genauso ab wie ein
fehlender Wert. Dass dafuer der Host-Header dient, ist unbedenklich -
es ist eine Diagnose, keine Entscheidung.

ERGAENZT §12.4
Die Rollout-Falle der Sitzungsgeneration: Sitzungen aus der Zeit vor der
Migration muessen weitergelten, sonst wirft die Einfuehrung jeden
Angemeldeten hinaus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-09-21 17:24:06 +02:00
co-authored by Claude Opus 5
parent df67932019
commit 12fb06fb48
+235 -8
View File
@@ -5,8 +5,18 @@
Dieses Dokument beschreibt die Hardwarekey-Anmeldung so, wie sie im Dieses Dokument beschreibt die Hardwarekey-Anmeldung so, wie sie im
Deploymentcenter läuft, und zwar mit dem Ziel, sie in einem anderen Projekt Deploymentcenter läuft, und zwar mit dem Ziel, sie in einem anderen Projekt
**gleichwertig** nachzubauen. Es ist bewusst konzeptlastig: Der PHP-Code ist **gleichwertig** nachzubauen. Es ist bewusst konzeptlastig: Der PHP-Code ist
die Referenz, nicht der Kern. Der Kern sind die Entscheidungen — und die Referenz, nicht der Kern. Der Kern sind die Entscheidungen.
besonders die drei Dinge, die *wie* eine Prüfung aussehen und keine sind.
Es deckt zwei Ebenen ab, und die zweite ist die, die man zuerst übersieht:
1. **Die Zeremonie** (§2–§7): die Dinge, die *wie* eine Prüfung aussehen und
keine sind, und was stattdessen trägt.
2. **Die Wege daneben** (§12): ein zweiter Anmeldepfad, der den Faktor nicht
kannte; eine Prüfung, die bei Datenbankfehlern stillschweigend abschaltete;
eine übernommene Sitzung, die die Anmeldemittel ändern durfte.
Dazu §13 (welche Entscheidung welche Zertifizierungsanforderung erfüllt) und
§14 (wie man Eigenschaften belegt, die sich nicht unit-testen lassen).
--- ---
@@ -481,6 +491,12 @@ einer offenen:
geprüft geprüft
- [ ] Anmeldefehler sind ununterscheidbar (falscher Name, falsches Passwort, - [ ] Anmeldefehler sind ununterscheidbar (falscher Name, falsches Passwort,
gesperrt, deaktiviert) gesperrt, deaktiviert)
- [ ] Eine Diagnose meldet **vor** dem Anmelden, ob der Anker gesetzt ist
*und ob er zur tatsächlichen Adresse passt* (§12.6)
- [ ] Sitzungen aus der Zeit vor der Generations-Migration gelten weiter
(§12.4) — sonst wirft die Einführung jeden Angemeldeten hinaus
- [ ] Die negativen Tests prüfen den **Ablehnungsgrund**, nicht nur die
Ablehnung (§14.1)
Der letzte Punkt steht nicht aus Höflichkeit dort: Genau hier hatte diese Der letzte Punkt steht nicht aus Höflichkeit dort: Genau hier hatte diese
Umsetzung einen Fehler. Die API verpackt Fehler als Umsetzung einen Fehler. Die API verpackt Fehler als
@@ -502,9 +518,12 @@ Begründung der Ablehnung stehen sollte.
| `src/Core/Webauthn/CredentialRepo.php` | Schlüssel und Notfalltoken | | `src/Core/Webauthn/CredentialRepo.php` | Schlüssel und Notfalltoken |
| `src/Core/Webauthn/roots/yubico.pem` | Die drei Wurzeln | | `src/Core/Webauthn/roots/yubico.pem` | Die drei Wurzeln |
| `public/api/auth/v1/index.php` | Die vier Aktionen | | `public/api/auth/v1/index.php` | Die vier Aktionen |
| `sql/migrations/019_*.sql`, `021_*.sql` | Datenmodell | | `public/login.php` | Zweite Stufe, Notfalltoken **und** Sperrbildschirm (§12.8) |
| `tests/e2e/suite_hardware.py` | Fünf Fälle, ohne Krypto-Bibliothek | | `public/api/health.php` | Ankerdiagnose `checks.webauthn_anchor` (§12.6) |
| `docs/UPGRADE.md` §17, §18 | Einführung und Verschärfung im Betrieb | | `sql/migrations/019_*.sql`, `021_*.sql` | Datenmodell des Schlüssels |
| `sql/migrations/022_*.sql` | `session_epoch` (§12.4) |
| `tests/e2e/suite_hardware.py` | Gruppen `hardware` und `haertung`, ohne Krypto-Bibliothek |
| `docs/UPGRADE.md` §17–§19 | Einführung, Verschärfung, Härtung im Betrieb |
--- ---
@@ -593,6 +612,20 @@ Wechsel erhöht. Die eigene Sitzung übernimmt den neuen Stand — wer sein
Passwort ändert, soll sich nicht selbst hinauswerfen. Ein **Zähler**, keine Passwort ändert, soll sich nicht selbst hinauswerfen. Ein **Zähler**, keine
Uhr: kein Zeitzonen- und kein Gleichstandsproblem. Uhr: kein Zeitzonen- und kein Gleichstandsproblem.
**Die Rollout-Falle:** Sitzungen, die vor der Migration entstanden sind,
tragen den Wert gar nicht. Wer sie als ungültig behandelt, wirft im Moment
der Einführung jeden Angemeldeten hinaus — und eine Sicherheitsverbesserung,
die das tut, wird beim nächsten Mal abgeschaltet. Fehlt der Wert, gilt die
Sitzung weiter:
```php
if (!isset($_SESSION['dc_session_epoch'])) {
return true; // aus der Zeit vor der Migration
}
```
Die Lücke schließt sich von selbst: Jede neue Anmeldung trägt den Stand ein.
### 12.5 Leerlauf: sperren, nicht beenden ### 12.5 Leerlauf: sperren, nicht beenden
AAL2 verlangt erneute Authentifizierung nach 30 Minuten Untätigkeit und lässt AAL2 verlangt erneute Authentifizierung nach 30 Minuten Untätigkeit und lässt
@@ -608,9 +641,37 @@ hält ein offener Tab mit Hintergrundabrufen die Sperre ewig auf.
`rpId` und `expectedOrigin` aus dem Host-Header abzuleiten heißt, den `rpId` und `expectedOrigin` aus dem Host-Header abzuleiten heißt, den
Maßstab der Prüfung vom Geprüften setzen zu lassen. Fest konfigurieren, und Maßstab der Prüfung vom Geprüften setzen zu lassen. Fest konfigurieren, und
bei fehlender Konfiguration **werfen** statt raten. Dazu eine Anzeige, die bei fehlender Konfiguration **werfen** statt raten.
das sichtbar macht, bevor jemand sich anmeldet — sonst tauschen Sie eine
stille Schwäche gegen eine stille Aussperrung. Damit tauschen Sie aber eine stille Schwäche gegen eine stille Aussperrung:
Ohne den Wert nimmt die Anlage keinen Hardwarekey mehr an, und gemerkt wird
das beim nächsten Anmeldeversuch. Es braucht also eine Diagnose, die das
**vorher** sichtbar macht — im Deploymentcenter unter
`/api/health` → `checks.webauthn_anchor`.
**Und diese Diagnose muss vergleichen, nicht nur nachsehen.** Die erste
Fassung meldete, *ob* ein Wert gesetzt ist. Das beantwortet die Frage nicht:
Ein Tippfehler in der Adresse weist den Schlüssel genauso ab wie ein
fehlender Wert, weil der Browser den echten Ursprung in `clientDataJSON`
einträgt und der zu `expectedOrigin` passen muss.
```php
$erwarteterHost = parse_url($appUrl, PHP_URL_HOST);
$tatsaechlicherHost = explode(':', $_SERVER['HTTP_HOST'] ?? '')[0];
$checks['webauthn_anchor'] = [
'ok' => $gesetzt && $passt,
'configured' => $gesetzt,
'matches_request' => $passt,
];
```
Dass hier der Host-Header herangezogen wird, ist unbedenklich: Es ist eine
**Diagnose, keine Entscheidung**. Wer ihn fälscht, erzeugt eine falsche
Warnung und sonst nichts — während die Zeremonie ihn unverändert ignoriert.
Die Unterscheidung lohnt sich zu merken: Derselbe Wert darf an der einen
Stelle verboten und an der anderen nützlich sein, solange klar ist, welche
von beiden etwas entscheidet.
### 12.7 Kleinigkeiten mit Hebelwirkung ### 12.7 Kleinigkeiten mit Hebelwirkung
@@ -630,3 +691,169 @@ stille Schwäche gegen eine stille Aussperrung.
- **Sperren auch auf der zweiten Stufe.** Sonst ist der Zwischenzustand - **Sperren auch auf der zweiten Stufe.** Sonst ist der Zwischenzustand
zwischen Passwort und Faktor die Stelle, an der unbegrenzt geraten werden zwischen Passwort und Faktor die Stelle, an der unbegrenzt geraten werden
darf — also die schwächste, obwohl sie die stärkere sein soll. darf — also die schwächste, obwohl sie die stärkere sein soll.
### 12.8 Ein Bildschirm für Leerlauf und Bestätigung
§12.3 und §12.5 verlangen dasselbe: „bitte noch einmal das Passwort". Es
liegt nahe, dafür zwei Dinge zu bauen — einen Sperrbildschirm für den
Leerlauf und ein Bestätigungsformular für heikle Aktionen. Tun Sie es nicht.
Zwei Stellen, die beide Passwörter prüfen, laufen mit der Zeit auseinander,
und beim Prüfen von Passwörtern ist das die teuerste Art von Duplikat.
Der Trick ist, den Leerlauf-Zustand *willentlich* herbeizuführen:
```php
// Bei Leerlauf: sperren statt vernichten
if (($now - $lastSeen) > self::IDLE_TIMEOUT) {
$_SESSION['dc_locked'] = true;
return;
}
// Vor einer heiklen Aktion: denselben Zustand herstellen
public static function lockForReauth(): void
{
$_SESSION['dc_locked'] = true;
unset($_SESSION['dc_reauth_at']);
}
```
Die ganze Wirkung hängt dann an einer Zeile in `isLoggedIn()`:
```php
if (!empty($_SESSION['dc_locked'])) {
return false; // eine gesperrte Sitzung ist keine Anmeldung
}
```
Jede Stelle, die bisher `isLoggedIn()` gefragt hat — WebUI, API, Endpunkte —
bekommt damit ohne eigenes Zutun die richtige Antwort. Sie müssen nichts
aufzählen, und deshalb können Sie auch nichts vergessen.
Zwei Feinheiten, die sonst beißen:
- **Die Weiterleitung muss unterscheiden.** Eine gesperrte Sitzung gehört auf
den Sperrbildschirm (Passwort genügt), eine fehlende auf die Anmeldung
(Passwort *und* Faktor). Derselbe Ort für beides wäre entweder zu streng
oder zu lasch.
- **Die Formulardaten gehen verloren.** Nach dem Entsperren landet man wieder
auf der Seite, nicht in der abgebrochenen Aktion. Sagen Sie das in der
Meldung, sonst sucht man nach einer Wirkung, die nie eingetreten ist.
---
## 13. Was welche Zertifizierungsanforderung erfüllt
Nützlich, wenn das Zielprojekt ein Sicherheitsprotokoll hat: Die Zahlen in
diesem Dokument sind nicht gegriffen, sie stammen aus Anforderungen. Wer sie
ändert, sollte wissen, welche davon er verlässt.
| Entscheidung | Wert hier | Woher |
|---|---|---|
| Zweiter Faktor kryptographisch, phishing-resistent | WebAuthn | NIST SP 800-63B, AAL2 (Phishing-Resistenz verlangt erst AAL3 — WebAuthn liefert sie ohnehin) |
| Erneute Authentifizierung bei Untätigkeit | 30 Min | AAL2; **ein** Faktor genügt zur Reaktivierung |
| Absolute Sitzungsgrenze | 12 Std | AAL2 |
| Mindestlänge Passwort | 12 Zeichen | PCI DSS 4.0, 8.3.6 |
| Kontosperre nach N Fehlversuchen | 5 | PCI DSS 4.0, 8.3.4 verlangt höchstens 10 |
| MFA auf **allen** Zugangswegen | §12.1 | PCI DSS 4.0, 8.4.2 — der Befund, der die Setup-API betraf |
| Erneute Authentifizierung vor Änderung der Anmeldemittel | 10 Min | OWASP ASVS, Kapitel Authentifizierung |
| Sitzungen bei Passwortwechsel beenden | §12.4 | OWASP ASVS, Kapitel Sitzungsverwaltung |
| HSTS, TLS-Zwang, Sicherheits-Header | §12.7 | OWASP ASVS, Kapitel Konfiguration |
| Protokollierung sicherheitsrelevanter Ereignisse | SecurityLogger | ISO 27001:2022 A.8.15 |
| Authentisierungsinformation, sichere Authentisierung | §12.1–§12.4 | ISO 27001:2022 A.5.17, A.8.5 |
| Identitäts- und Berechtigungsmanagement | Rollentrennung §12.1 | BSI IT-Grundschutz ORP.4, APP.3.1 |
**Zwei Ehrlichkeiten dazu:**
- Eine Begutachtung, die nur die Authentifizierungskapitel betrachtet, kann
auch nur über diese eine Aussage treffen. „Die genannten Blocker sind
geschlossen" ist belegbar; „ASVS L2 bestanden" wäre es nicht.
- ISO 27001 zertifiziert das Managementsystem, nicht den Code. Was hier steht,
ist Nachweismaterial für einen Auditor — kein Zertifikat.
### 13.1 Wo die Anforderungen sich widersprechen
Das ist der interessante Teil. AAL2 verlangt Reauthentifizierung alle 30
Minuten. Nimmt man das als „volle Anmeldung mit Hardwarekey", entsteht ein
Verhalten, das der Standard gerade nicht will: Der Schlüssel bleibt stecken,
weil ihn jemand achtmal am Tag sucht. Derselbe Standard lässt deshalb **einen**
Faktor zur Reaktivierung genügen.
> Eine Regel, die zur Umgehung erzieht, ist schlechter als eine mildere, die
> eingehalten wird.
Wer im Zielprojekt eine Zahl verschärft, sollte diese Frage mitstellen:
Welches Verhalten erzeugt sie bei dem Menschen, der sie täglich einhalten
muss?
---
## 14. Wie man belegt, was sich nicht unit-testen lässt
Drei der wichtigsten Zusicherungen in diesem Dokument lassen sich nicht mit
einem gewöhnlichen Testfall prüfen. Sie sind trotzdem belegbar — nur anders.
### 14.1 Der Test muss aus dem richtigen Grund scheitern
Die Probe aus §2.3 (selbst ausgestelltes Zertifikat) ist wertlos, wenn die
Ablehnung wegen der **Signatur** kommt: Dann hat der Test sich selbst falsch
gebaut und über die Kette nichts gezeigt. Prüfen Sie also nicht nur *dass*
abgelehnt wurde, sondern **womit**:
```python
if 'signatur' in meldung.lower():
return 'UNKLAR: an der Signatur gescheitert - beweist nichts ueber die Kette'
if 'wurzel' in meldung.lower() or 'kette' in meldung.lower():
return 'BESTANDEN'
```
Das gilt allgemein: Ein negativer Sicherheitstest, der den Ablehnungsgrund
nicht prüft, bestätigt irgendwann nur noch, dass irgendetwas kaputt ist.
### 14.2 Manche Eigenschaften verlangen echtes Warten
Ob eine **veraltete** Sitzung die Anmeldemittel noch ändern kann, hängt am
Zehn-Minuten-Fenster. Es für den Test zu verkleinern liegt nahe — und prüft
dann eine andere Anlage als die, die später läuft. Im Deploymentcenter
wartet ein Werkzeug außerhalb der Suite die vollen elf Minuten ab und prüft
danach:
```
A) Formularweg -> Sperrbildschirm
B) Zustand danach -> unverändert (die Aktion hatte keine Wirkung)
C) JSON-Weg -> 403 reauth_required
D) Sperrbildschirm -> wird angezeigt
E) Passwort -> öffnet wieder
```
Punkt B ist der eigentliche Beweis. „Wurde umgeleitet" heißt noch nicht
„hatte keine Wirkung" — eine Umleitung nach getaner Arbeit sieht genauso aus.
**Eine Falle aus eigener Erfahrung:** Beim ersten Anlauf schickte die Probe
beide Wege durch dieselbe Sitzung. Schritt A hatte sie bereits gesperrt, also
meldete der JSON-Weg folgerichtig „nicht angemeldet" statt
„reauth_required" — ein Fehler der Probe, nicht des Codes. Zwei Zustände, die
sich gegenseitig ausschließen, brauchen zwei Sitzungen.
### 14.3 Was sich gar nicht nachstellen lässt
Der Gegenfall zu §2.3 — ein echter Yubikey, der eine gültige Kette vorlegt —
ist von einem Testrechner aus nicht erzeugbar; dafür bräuchte man den privaten
Wurzelschlüssel des Herstellers, und genau darauf beruht die ganze Aussage.
Diese Hälfte belegt nur echte Hardware. Planen Sie den Handgriff ein, statt
ihn wegzuautomatisieren.
### 14.4 Was in die automatische Reihe gehört
Alles, was ohne Kryptographie und ohne Warten auskommt — und das ist mehr,
als es zunächst scheint. Bei einer Registrierung mit `fmt: "none"` prüft der
Server **keine Signatur**; es genügt ein fest eingebauter, gültiger
Kurvenpunkt. Damit lässt sich der entscheidende Vergleich ohne jede
Abhängigkeit fahren:
> Dieselben Bytes eines Authenticators ohne Herkunftsnachweis — einmal mit
> und einmal ohne Regel.
Ebenso automatisierbar: dass die Anmeldung die Existenz eines Kontos nicht
verrät, dass HSTS und CSP ankommen, dass die Diagnose aus §12.6 grün meldet,
und dass ein Konto der falschen Rolle am zweiten Anmeldepfad abgewiesen wird.