Automatischer Push: 14 Datei(en) geändert
- .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/bremse_echtlauf.py - .vault-tools/trading-plattform-pruefung/browser_w.py - .vault-tools/trading-plattform-pruefung/generalprobe_echtlauf.py - .vault-tools/trading-plattform-pruefung/ha_aktiv_setzen.py - .vault-tools/trading-plattform-pruefung/handelstag_auswertung.py - .vault-tools/trading-plattform-pruefung/hausmelder_echtlauf.py - .vault-tools/trading-plattform-pruefung/killswitch_simulation.py - .vault-tools/trading-plattform-pruefung/killswitch_simulation_mutanten.py - .vault-tools/trading-plattform-pruefung/latenz_auswertung.py - .vault-tools/trading-plattform-pruefung/s1_alpaca_lesen.py - .vault-tools/trading-plattform-pruefung/s5_echtlauf.py - … und 2 weitere Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
246af19683
commit
593acdd122
14 files changed
+2090
-5
No files matched your search
@@ -0,0 +1,192 @@
|
||||
"""Browserlauf zu Paket W: echter Chromium mit virtuellem Authenticator gegen die Probe-Instanz.
|
||||
|
||||
Koordinator-Pruefcode. Laeuft unter Windows mit Playwright. Ablauf: mit Passwort und Code anmelden, in
|
||||
"Zugang" einen Schluessel registrieren, abmelden, mit dem Schluessel anmelden (der Zaehler des virtuellen
|
||||
Authenticators muss steigen -- sonst waere er nie gefragt worden), abmelden, wieder mit Code anmelden,
|
||||
Schluessel entfernen. Dazu die Gegenprobe unter 127.0.0.1 und ein Blick in Schluesseldatei und
|
||||
Anmeldeprotokoll der Probe-Instanz.
|
||||
|
||||
Aufruf: python browser_w.py <port> <ausgabeordner-der-probe>
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
PORT = int(sys.argv[1])
|
||||
ORDNER = Path(sys.argv[2])
|
||||
ZUGANG = json.loads((ORDNER / "zugang.json").read_text(encoding="utf-8"))
|
||||
KNOPF_SCHLUESSEL = "Mit Sicherheitsschlüssel anmelden"
|
||||
|
||||
befunde: list[str] = []
|
||||
schritte: list[str] = []
|
||||
|
||||
|
||||
def totp(geheimnis: str) -> tuple[str, int]:
|
||||
schluessel = base64.b32decode(geheimnis)
|
||||
zaehler = int(time.time() // 30)
|
||||
mac = hmac.new(schluessel, struct.pack(">Q", zaehler), hashlib.sha1).digest()
|
||||
versatz = mac[-1] & 0x0F
|
||||
wert = (struct.unpack(">I", mac[versatz:versatz + 4])[0] & 0x7FFFFFFF) % 1_000_000
|
||||
return f"{wert:06d}", zaehler
|
||||
|
||||
|
||||
def schritt(text: str, ok: bool) -> None:
|
||||
schritte.append(("OK " if ok else "BEFUND ") + text)
|
||||
if not ok:
|
||||
befunde.append(text)
|
||||
|
||||
|
||||
def melde_mit_code_an(seite, letzter_zaehler: int) -> int:
|
||||
# Ein Code gilt je Zeitschritt einmal; vor der zweiten Anmeldung den naechsten Schritt abwarten.
|
||||
while int(time.time() // 30) <= letzter_zaehler:
|
||||
time.sleep(1.0)
|
||||
code, zaehler = totp(ZUGANG["geheimnis"])
|
||||
seite.fill("#anmeldung-benutzer", ZUGANG["benutzer"])
|
||||
seite.fill("#anmeldung-passwort", ZUGANG["passwort"])
|
||||
seite.fill("#anmeldung-code", code)
|
||||
seite.get_by_role("button", name="Anmelden", exact=True).click()
|
||||
seite.get_by_role("button", name="Abmelden").wait_for(timeout=15000)
|
||||
return zaehler
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
kontext = browser.new_context()
|
||||
seite = kontext.new_page()
|
||||
konsole: list[str] = []
|
||||
api: list[str] = []
|
||||
seite.on("console", lambda m: konsole.append(f"{m.type}: {m.text}") if m.type in ("error", "warning") else None)
|
||||
seite.on("pageerror", lambda e: konsole.append(f"pageerror: {e}"))
|
||||
seite.on("response", lambda r: api.append(f"{r.status} {r.request.method} {r.url.split(str(PORT), 1)[-1]}") if "/api/" in r.url else None)
|
||||
|
||||
cdp = kontext.new_cdp_session(seite)
|
||||
cdp.send("WebAuthn.enable")
|
||||
authenticator = cdp.send("WebAuthn.addVirtualAuthenticator", {"options": {
|
||||
"protocol": "ctap2", "transport": "usb", "hasResidentKey": False,
|
||||
"hasUserVerification": True, "isUserVerified": True, "automaticPresenceSimulation": True,
|
||||
}})["authenticatorId"]
|
||||
|
||||
def zugaenge() -> list[dict]:
|
||||
return cdp.send("WebAuthn.getCredentials", {"authenticatorId": authenticator})["credentials"]
|
||||
|
||||
seite.goto(f"http://localhost:{PORT}/")
|
||||
seite.locator("#anmeldung-benutzer").wait_for(timeout=15000)
|
||||
schritt("Anmeldemaske unter http://localhost erreichbar", True)
|
||||
knopf = seite.get_by_role("button", name=KNOPF_SCHLUESSEL)
|
||||
schritt("Knopf fuer den Schluessel ist da und bedienbar", knopf.count() == 1 and knopf.is_enabled())
|
||||
|
||||
zaehler = melde_mit_code_an(seite, -1)
|
||||
schritt("Anmeldung mit Passwort und Code", True)
|
||||
|
||||
seite.get_by_text("Zugang", exact=True).first.click()
|
||||
seite.locator("#zugang-bezeichnung").wait_for(timeout=10000)
|
||||
seite.fill("#zugang-bezeichnung", "Virtueller Schluessel")
|
||||
seite.fill("#zugang-passwort", ZUGANG["passwort"])
|
||||
seite.get_by_role("button", name="Schlüssel hinzufügen").click()
|
||||
liste = seite.get_by_role("list", name="Sicherheitsschlüssel")
|
||||
liste.wait_for(timeout=15000)
|
||||
schritt("Registrierung: der Schluessel steht in der Liste", "Virtueller Schluessel" in liste.inner_text())
|
||||
vorher = zugaenge()
|
||||
schritt("Der virtuelle Authenticator haelt genau einen Zugang", len(vorher) == 1)
|
||||
zaehler_vorher = vorher[0]["signCount"] if vorher else -1
|
||||
|
||||
# Schluesseldatei ansehen, solange der Schluessel hinterlegt ist.
|
||||
datei = ORDNER / "zugang-schluessel.json"
|
||||
kandidaten = [pfad for pfad in ORDNER.glob("*schluessel*")]
|
||||
schritt(f"Schluesseldatei liegt neben der Zugangsdatei ({[k.name for k in kandidaten]})", bool(kandidaten))
|
||||
if kandidaten:
|
||||
inhalt = kandidaten[0].read_text(encoding="utf-8")
|
||||
schritt("Schluesseldatei traegt weder Passwort noch TOTP-Geheimnis",
|
||||
ZUGANG["passwort"] not in inhalt and ZUGANG["geheimnis"] not in inhalt)
|
||||
daten = json.loads(inhalt)
|
||||
print("Schluesseldatei, Form:", json.dumps(
|
||||
{k: (sorted(v[0]) if isinstance(v, list) and v and isinstance(v[0], dict) else type(v).__name__)
|
||||
for k, v in (daten.items() if isinstance(daten, dict) else {"liste": daten}.items())},
|
||||
ensure_ascii=False)[:600])
|
||||
|
||||
seite.get_by_role("button", name="Abmelden").click()
|
||||
seite.locator("#anmeldung-benutzer").wait_for(timeout=10000)
|
||||
seite.fill("#anmeldung-benutzer", ZUGANG["benutzer"])
|
||||
seite.fill("#anmeldung-passwort", ZUGANG["passwort"])
|
||||
seite.get_by_role("button", name=KNOPF_SCHLUESSEL).click()
|
||||
seite.get_by_role("button", name="Abmelden").wait_for(timeout=15000)
|
||||
schritt("Anmeldung mit Passwort und Sicherheitsschluessel, ohne Code", True)
|
||||
nachher = zugaenge()
|
||||
zaehler_nachher = nachher[0]["signCount"] if nachher else -1
|
||||
schritt(f"Der Authenticator wurde wirklich gefragt (Zaehler {zaehler_vorher} -> {zaehler_nachher})",
|
||||
zaehler_nachher > zaehler_vorher)
|
||||
|
||||
# Falsches Passwort auf dem Schluessel-Weg: Ablehnung, keine Sitzung.
|
||||
seite.get_by_role("button", name="Abmelden").click()
|
||||
seite.locator("#anmeldung-benutzer").wait_for(timeout=10000)
|
||||
seite.fill("#anmeldung-benutzer", ZUGANG["benutzer"])
|
||||
seite.fill("#anmeldung-passwort", "ganz-sicher-falsch-2026")
|
||||
seite.get_by_role("button", name=KNOPF_SCHLUESSEL).click()
|
||||
hinweis = seite.get_by_role("alert")
|
||||
hinweis.wait_for(timeout=10000)
|
||||
schritt("Falsches Passwort auf dem Schluessel-Weg wird abgelehnt", "stimmt nicht" in hinweis.inner_text())
|
||||
|
||||
zaehler = melde_mit_code_an(seite, zaehler)
|
||||
schritt("Der Code-Weg geht danach unveraendert", True)
|
||||
|
||||
seite.get_by_text("Zugang", exact=True).first.click()
|
||||
seite.locator("#zugang-passwort").wait_for(timeout=10000)
|
||||
seite.fill("#zugang-passwort", ZUGANG["passwort"])
|
||||
seite.get_by_role("button", name="Entfernen").click()
|
||||
seite.get_by_text("Es ist kein Sicherheitsschlüssel hinterlegt").wait_for(timeout=10000)
|
||||
schritt("Entfernen: die Liste ist wieder leer", True)
|
||||
|
||||
# Gegenprobe unter 127.0.0.1: beginn antwortet mit 409, kein 500, kein leerer Schirm.
|
||||
zweite = kontext.new_page()
|
||||
zweite.goto(f"http://127.0.0.1:{PORT}/")
|
||||
zweite.locator("#anmeldung-benutzer").wait_for(timeout=15000)
|
||||
antwort = zweite.evaluate(
|
||||
"""async ([benutzer, passwort]) => {
|
||||
const r = await fetch('/api/anmeldung/schluessel/beginn', {method: 'POST',
|
||||
headers: {'content-type': 'application/json'},
|
||||
body: JSON.stringify({benutzer, passwort})});
|
||||
return [r.status, await r.text()];
|
||||
}""", [ZUGANG["benutzer"], ZUGANG["passwort"]])
|
||||
schritt(f"Unter 127.0.0.1 antwortet beginn mit 409 ({antwort[0]})", antwort[0] == 409)
|
||||
print("Satz unter 127.0.0.1:", antwort[1][:260])
|
||||
browser.close()
|
||||
|
||||
protokoll = ORDNER / "anmeldungen.jsonl"
|
||||
zeilen = [json.loads(z) for z in protokoll.read_text(encoding="utf-8").splitlines() if z.strip()]
|
||||
wege = [z.get("weg") for z in zeilen]
|
||||
print(f"Anmeldeprotokoll: {len(zeilen)} Zeilen, Ausgaenge {[z.get('ausgang') for z in zeilen]}, Wege {wege}")
|
||||
roh = protokoll.read_text(encoding="utf-8")
|
||||
schritt("Anmeldeprotokoll traegt eine Zeile mit weg=schluessel", "schluessel" in wege)
|
||||
schritt("Anmeldeprotokoll traegt weder Passwort noch Geheimnis",
|
||||
ZUGANG["passwort"] not in roh and ZUGANG["geheimnis"] not in roh and "ganz-sicher-falsch" not in roh)
|
||||
|
||||
boese = [z for z in konsole if "Content Security Policy" in z or "Permissions" in z or "pageerror" in z]
|
||||
schritt(f"Keine CSP-, Permissions-Policy- oder Seitenfehler in der Konsole ({len(boese)})", not boese)
|
||||
fuenfhunderter = [z for z in api if z.startswith("5")]
|
||||
schritt(f"Keine 5xx-Antwort der Schnittstelle ({len(fuenfhunderter)})", not fuenfhunderter)
|
||||
|
||||
print("--- Schritte")
|
||||
for zeile in schritte:
|
||||
print(zeile)
|
||||
print("--- API-Antworten der Schluessel-Wege")
|
||||
for zeile in api:
|
||||
if "schluessel" in zeile or "anmeldung" in zeile:
|
||||
print(" " + zeile)
|
||||
print("--- Konsole (Fehler und Warnungen)")
|
||||
for zeile in konsole[:12]:
|
||||
print(" " + zeile[:220])
|
||||
print(f"{len(befunde)} Befund(e): {befunde}")
|
||||
return 2 if befunde else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user