feat(2.9.2): Nur geprueft in Hardware entstandene Schluessel als zweiter Faktor

Befund aus dem Betrieb: Ein Passkey aus 1Password liess sich als
Hardwarekey registrieren. Fuer das Deploymentcenter hinnehmbar, fuer ein
Protokoll, das ausdruecklich Hardware fordert, nicht.

Zwei Dinge, die wie eine Pruefung aussahen und keine waren - beide seit
2.9.0 gesetzt: authenticatorAttachment "cross-platform" steuert, was der
Browser anbietet, und ein Passwortverwalter meldet sich ebenso. Und
response.authenticatorAttachment setzt der Client, also genau die Instanz,
gegen die sich nichts beweisen laesst.

Was traegt, ist Basic Attestation. Neu geprueft wird die Kette bis zu einer
hinterlegten Herstellerwurzel; die drei FIDO-Wurzeln von Yubico liegen bei.
Ein x5c allein genuegt ausdruecklich nicht: Der Fall "eigenes Zertifikat,
Name Yubico, AAGUID eines echten YubiKey 5, gueltige Unterschrift" wurde
nachgebaut und wird abgewiesen - an der Kette, nicht an der Signatur.

Drei Stufen statt eines Schalters, weil alle bisher registrierten
Schluessel als nicht nachgewiesen gelten - auch echte Yubikeys. Bei
attestation "none" ersetzt der Browser die Geraetekennung durch Nullbytes,
bevor der Server sie sieht; es gab also nie eine Herkunft. Ein einzelner
Schalter haette beim Umlegen den eigenen Zugang gesperrt. Der Wechsel nach
"strict" wird zusaetzlich abgelehnt, solange das eigene Konto keinen
nachgewiesenen Schluessel hat.

- Attestation.php: packed und fido-u2f, Kettenpruefung gegen eigene
  Wurzeln statt gegen den Speicher des Betriebssystems, Gueltigkeit,
  Gegenprobe der AAGUID aus der Zertifikatserweiterung
- HardwarePolicy.php: off / register / strict, optionale Modellliste,
  Wurzeln aus der Konfiguration erweiterbar
- Migration 021: attested und drei Anzeigespalten, Vorgabe 0
- WebUI: Herkunft je Schluessel, Regel vor der Beruehrung angesagt
- testctl clear_credentials, damit ein Testlauf den naechsten nicht
  aussperrt
- Testreihe "hardware", 5 Faelle, ohne Krypto-Bibliothek

184 von 184 Testfaellen gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-09-21 10:44:19 +02:00
co-authored by Claude Opus 5
parent 8f514eb17a
commit 073d52ab7d
14 changed files with 1978 additions and 33 deletions
+111
View File
@@ -674,3 +674,114 @@ alten Namens werden dabei aufgeräumt.
- [ ] Roadmap → 🧭 Tafel zeigt Spalten, Spuren und Kärtchen
- [ ] Ein Klick auf ein Kärtchen öffnet das Panel, Kommentieren funktioniert
- [ ] `python tests/e2e/run.py` gegen die Testinstanz: alles grün
---
## 18. Umstellung auf 2.9.2 — nur echte Hardware als zweiter Faktor
### 18.1 Worum es geht
Seit 2.9.0 verlangt die Anmeldung einen Hardwarekey. Was sie *nicht* verlangte,
war, dass es wirklich Hardware ist: Ein Passkey aus einem Passwortverwalter
(1Password, Bitwarden, iCloud, Google) ließ sich ebenso registrieren wie ein
Yubikey. Für das Deploymentcenter selbst ist das hinnehmbar. Wo ein
Sicherheitsprotokoll ausdrücklich Hardware fordert, ist es der Unterschied
zwischen einer Regel und einem Wunsch.
Zwei Dinge, die *wie* eine Prüfung aussehen und keine sind — beide waren in
2.9.0 bereits gesetzt:
- `authenticatorAttachment: "cross-platform"` steuert, was der Browser
**anbietet**. Ein Passwortverwalter meldet sich ebenfalls als
cross-platform.
- `response.authenticatorAttachment` setzt der **Client**, und der Client ist
genau die Instanz, gegen die sich hier nichts beweisen lässt.
Was trägt, ist die Attestation: Der Authenticator unterschreibt bei der
Aufnahme mit einem Schlüssel, der ab Werk in seinem Sicherheitselement liegt
und es nie verlässt. Das zugehörige Zertifikat muss auf eine hinterlegte
Herstellerwurzel ketten.
**Ein Zertifikat allein genügt nicht.** Jeder kann sich eines ausstellen, es
„Yubico U2F EE“ nennen, die AAGUID eines echten YubiKey 5 eintragen und sauber
unterschreiben. Genau dieser Fall wurde nachgebaut und gegen den Server
gefahren: Er wird abgewiesen — an der **Kette**, nicht an der Signatur. Was
Software nicht herstellen kann, ist eine Kette, die bei Yubicos Wurzel endet.
### 18.2 Migration
```bash
php cli/migrate.php # oder System → 🗄️ DB Migration im WebUI
```
Migration 021 ergänzt `dc_user_credentials` um `attested`, `attestation_fmt`,
`attestation_ca` und `attestation_root`. Sie setzt **keine** Regel — ohne
Umstellen bleibt das Verhalten von 2.9.1.
### 18.3 Die drei Stufen
| Stufe | Aufnahme neuer Schlüssel | Anmeldung mit vorhandenen |
|---|---|---|
| `off` | jeder Authenticator | alle |
| `register` | nur mit Herstellernachweis | alle |
| `strict` | nur mit Herstellernachweis | nur nachgewiesene |
Einzustellen unter **System → 👤 Benutzer → 🛡️ Welche Schlüssel zugelassen
sind**.
### 18.4 Der Weg nach `strict` — in dieser Reihenfolge
> **Alle vor 2.9.2 registrierten Schlüssel gelten als nicht nachgewiesen —
> auch echte Yubikeys.** Das ist keine Vorsicht, sondern eine Tatsache des
> Standards: Bei `attestation: "none"` ersetzt der **Browser** die
> Gerätekennung durch Nullbytes, bevor der Server sie zu sehen bekommt. Es gab
> also nie eine Herkunft, nicht einmal eine ungeprüfte.
1. Stufe auf **`register`** stellen.
2. Den Hardwarekey **einmal neu aufnehmen**. In der Liste muss danach
*🔒 Hardware* stehen, mit der erreichten Wurzel.
3. Den alten, nicht nachgewiesenen Eintrag entfernen.
4. Erst jetzt auf **`strict`**.
Schritt 4 wird abgelehnt, solange das eigene Konto keinen nachgewiesenen
Schlüssel hat — eine Einstellung, die sich selbst aussperrt, ist keine
Einstellung, sondern eine Falle.
### 18.5 Wenn doch etwas klemmt
- **Der Browser fragt nach Marke und Modell.** Das ist der sichtbare Preis von
`attestation: "direct"` und kein Fehler. Wer ablehnt, liefert keine
Attestation und wird abgewiesen.
- **Ausgesperrt.** Der Notfalltoken funktioniert unabhängig von der Regel.
Danach unter System → Benutzer die Stufe zurückstellen.
- **Ganz ohne WebUI:**
```sql
DELETE FROM dc_settings WHERE skey = 'webauthn_hardware_policy';
```
Fehlt der Eintrag, gilt wieder `off`.
### 18.6 Andere Hersteller als Yubico
Mitgeliefert sind die drei FIDO-Wurzeln von Yubico
(`src/Core/Webauthn/roots/yubico.pem`). Weitere Wurzeln in der `config.php`:
```php
'security' => [
'webauthn_root_files' => ['/etc/dc/solokeys.pem'],
],
```
Bewusst dort und nicht im WebUI: Eine Vertrauenswurzel über dieselbe
Oberfläche austauschbar zu machen, die sie schützt, hieße die Prüfung dort
aushebelbar zu machen.
### 18.7 Prüfen, ob alles läuft
- [ ] Migration 021 ist durch (Spalte `attested` in `dc_user_credentials`)
- [ ] Stufe `register`: Der Yubikey wird aufgenommen und zeigt *🔒 Hardware*
- [ ] Stufe `register`: Ein Passwortverwalter wird mit einer Meldung abgewiesen,
die den Grund nennt
- [ ] `strict` lässt sich **nicht** einschalten, solange kein nachgewiesener
Schlüssel am eigenen Konto hängt
- [ ] Nach `strict`: Anmeldung mit dem Yubikey funktioniert weiter
- [ ] `python tests/e2e/run.py --gruppe hardware`: alles grün