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:
co-authored by
Claude Opus 5
parent
8f514eb17a
commit
073d52ab7d
@@ -176,6 +176,17 @@ switch ($action) {
|
||||
// Vor Migration 019 gibt es die Tabelle nicht.
|
||||
}
|
||||
|
||||
// Die Hardwarekey-Regel gehoert ebenfalls zurueckgesetzt. Sie steht
|
||||
// in dc_settings und ueberlebt das Loeschen der Konten sonst - ein
|
||||
// Lauf, der auf "strict" endet, liesse den naechsten an einer Regel
|
||||
// scheitern, die er gar nicht prueft. Ausgetragen statt auf "off"
|
||||
// gesetzt: Fehlt der Eintrag, liest HardwarePolicy die Vorgabe, und
|
||||
// das ist genau derselbe Zustand wie bei einer frischen Anlage.
|
||||
$db->prepare("
|
||||
DELETE FROM dc_settings
|
||||
WHERE skey IN ('webauthn_hardware_policy', 'webauthn_aaguid_allowlist')
|
||||
")->execute();
|
||||
|
||||
$db->prepare('DELETE FROM dc_users WHERE username = :u')->execute([':u' => $adminUser]);
|
||||
$db->prepare('
|
||||
INSERT INTO dc_users (username, password_hash, created_at)
|
||||
@@ -381,10 +392,43 @@ switch ($action) {
|
||||
Http::ok(['unlocked' => $username, 'affected' => $stmt->rowCount()]);
|
||||
break;
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Hardwarekeys eines Kontos entfernen, ohne die Datenbank zu leeren.
|
||||
//
|
||||
// Warum eigens und nicht ueber "reset": Ein registrierter zweiter Faktor
|
||||
// macht das Konto fuer einen automatisierten Lauf unbenutzbar - Passwort
|
||||
// allein fuehrt nur noch in den Zwischenzustand. Ein Testlauf, der einen
|
||||
// Schluessel aufnimmt, sperrt damit den naechsten aus. Der ganze
|
||||
// Datenbestand muss dafuer aber nicht weichen; "reset" wuerde Roadmap,
|
||||
// Meldungen und Lizenzen gleich mit loeschen, und das ist ein hoher Preis
|
||||
// fuer das Aufraeumen einer einzigen Tabelle.
|
||||
//
|
||||
// Nebenbei ist es der Weg zurueck, wenn man sich auf der Testinstanz mit
|
||||
// einer schaerferen Regel selbst ausgesperrt hat. Auf der Produktion gibt
|
||||
// es diese Datei nicht - dort ist der Notfalltoken der Weg.
|
||||
case 'clear_credentials': {
|
||||
$username = (string)(Http::str('username') ?? 'admin');
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare('
|
||||
DELETE FROM dc_user_credentials
|
||||
WHERE user_id IN (SELECT id FROM dc_users WHERE username = :u)
|
||||
');
|
||||
$stmt->execute([':u' => $username]);
|
||||
$entfernt = $stmt->rowCount();
|
||||
} catch (Throwable $e) {
|
||||
Http::fail(500, 'no_table', 'dc_user_credentials fehlt - Migration 019 steht aus.');
|
||||
}
|
||||
|
||||
Http::ok(['username' => $username, 'removed' => $entfernt]);
|
||||
break;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
default:
|
||||
Http::fail(400, 'unknown_action', sprintf('Unbekannte Aktion "%s".', $action), null, [
|
||||
'actions' => ['status', 'wipe', 'migrate', 'reset', 'workbench_token',
|
||||
'update_master_token', 'genkey', 'log', 'clearlog', 'unlock_user'],
|
||||
'update_master_token', 'genkey', 'log', 'clearlog', 'unlock_user',
|
||||
'clear_credentials'],
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user