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
@@ -0,0 +1,68 @@
-- Migration 021: Herkunft eines Hardwarekeys festhalten
--
-- Additive Migration. Der Migrator toleriert 1050/1060/1061/1062/1091.
--
-- ---------------------------------------------------------------------------
-- Warum das nicht schon in 019 stand
-- ---------------------------------------------------------------------------
-- 019 hat die Attestation bewusst nicht geprueft und deshalb auch nichts
-- darueber gespeichert: Wer seinen eigenen Schluessel an seinem eigenen Server
-- registriert, muss sich nicht ausweisen lassen. Die Spalte aaguid war dabei
-- schon angelegt - und blieb in der Praxis immer leer. Das ist kein Fehler
-- der Umsetzung, sondern Absicht des Standards: Fordert eine Seite
-- attestation "none" an, ersetzt der BROWSER die Geraetekennung durch
-- sechzehn Nullbytes, bevor der Server sie zu sehen bekommt. Ohne
-- angeforderte Attestation gibt es also grundsaetzlich keine Herkunft, nicht
-- einmal eine ungepruefte.
--
-- Gebraucht wird sie, seit die Frage anders lautet: Ein Passwortverwalter
-- liess sich als zweiter Faktor registrieren. Fuer das Deploymentcenter
-- selbst ist das hinnehmbar, fuer ein Protokoll, das ausdruecklich Hardware
-- verlangt, nicht.
--
-- ---------------------------------------------------------------------------
-- attested ist die einzige Spalte, an der eine Entscheidung haengt
-- ---------------------------------------------------------------------------
-- Die drei uebrigen sind Anzeige und Nachvollziehbarkeit. Die Trennung ist
-- beabsichtigt: attestation_ca und attestation_root kommen aus einem
-- Zertifikat, das der Authenticator geschickt hat - also von aussen. Sie
-- eignen sich, um einem Menschen zu zeigen, was da angekommen ist, aber nicht
-- als Grundlage eines Vergleichs im Code. Was die Pruefung ergeben hat, steht
-- in genau einem Bit, und das hat der Server selbst gesetzt.
--
-- Vorgabe 0: Alles, was vor dieser Migration registriert wurde, ist NICHT
-- attestiert - und zwar unabhaengig davon, ob es ein Yubikey war. Das ist die
-- ehrliche Vorgabe. Ein "war bestimmt Hardware" waere eine Behauptung ueber
-- etwas, das nie geprueft wurde, und genau solche Behauptungen soll diese
-- Migration abschaffen. Wer die Stufe "strict" nutzen will, nimmt seinen
-- Schluessel einmal neu auf; das ist der Preis dafuer, dass die Spalte etwas
-- bedeutet.
ALTER TABLE dc_user_credentials
ADD COLUMN attested TINYINT(1) NOT NULL DEFAULT 0 AFTER aaguid;
-- Das Attestationsformat, so wie der Authenticator es genannt hat:
-- "packed" (CTAP2, der Normalfall beim YubiKey 5), "fido-u2f" (CTAP1),
-- "none" (Passwortverwalter und eingebaute Passkeys).
ALTER TABLE dc_user_credentials
ADD COLUMN attestation_fmt VARCHAR(32) NULL AFTER attested;
-- Der Name aus dem Blattzertifikat - bei Yubico etwa
-- "Yubico U2F EE Serial 1234567890".
ALTER TABLE dc_user_credentials
ADD COLUMN attestation_ca VARCHAR(190) NULL AFTER attestation_fmt;
-- Die Wurzel, bei der die Kette geendet hat. Sie steht hier und nicht nur im
-- Protokoll, weil sie die Frage "warum gilt dieser Schluessel als Hardware"
-- ohne Blick in die Logdatei beantwortet.
ALTER TABLE dc_user_credentials
ADD COLUMN attestation_root VARCHAR(190) NULL AFTER attestation_ca;
-- ---------------------------------------------------------------------------
-- Die Regel selbst steht in dc_settings, nicht hier
-- ---------------------------------------------------------------------------
-- Eingetragen wird sie erst beim ersten Speichern in der Oberflaeche. Ein
-- INSERT waere hier moeglich, wuerde aber eine Entscheidung vorwegnehmen:
-- Eine Migration, die eine Sicherheitsstufe setzt, aendert im Vorbeigehen das
-- Verhalten einer laufenden Anlage. Fehlt der Eintrag, liest
-- HardwarePolicy::level() die Vorgabe "off" - also genau das Verhalten von
-- vorher. Umgestellt wird, wenn jemand umstellt.