#!/usr/bin/env python3
"""ProofKey L2 — Build Step Zero device probe (STANDALONE, one file).

Same job as scripts/l2_device_probe.py but with NO dependency on the Proofpane
backend — it inlines the tier logic (mirrors app.services.webauthn_registration
+ fido_mds_service) so you can run it on a fresh machine (e.g. a Windows box)
after only:

    pip install webauthn
    python l2_device_probe_standalone.py         # optionally: set FIDO_MDS_PATH first

Then open http://localhost:8799 and click "Probe this device". It registers a
passkey on THIS machine's authenticator (Windows Hello, Touch ID, a FIDO2 key)
and prints its empirical tier row: attestation fmt, AAGUID, whether it resolves
in FIDO MDS, whether the attestation cert chains to the MDS root, BE/BS/UV, and
the tier (l2_standard = "WebAuthn-backed" / l2_high = "hardware-backed").

Read-only: verifies + classifies, stores nothing.

FIDO_MDS_PATH: point it at a downloaded FIDO MDS BLOB to get real l2_high
classification. Download once:  curl -s https://mds3.fidoalliance.org/ -o mds.jwt
Unset => "in MDS?" is always No, so every device reads l2_standard.
"""
from __future__ import annotations

import base64
import http.server
import json
import os
import socketserver

from webauthn import (
    generate_registration_options, verify_registration_response, options_to_json,
)
from webauthn.helpers import validate_certificate_chain
from webauthn.helpers.structs import (
    AttestationConveyancePreference, AuthenticatorAttachment,
    AuthenticatorSelectionCriteria, ResidentKeyRequirement,
    UserVerificationRequirement,
)

PORT = int(os.environ.get("L2_PROBE_PORT", "8799"))
RP_ID = "localhost"
ORIGIN = f"http://localhost:{PORT}"
MDS_PATH = os.environ.get("FIDO_MDS_PATH") or None
# L2_PROBE_ATTACHMENT=platform  -> force the built-in authenticator (Touch ID / Windows Hello)
# L2_PROBE_ATTACHMENT=cross-platform -> force an external security key
# unset -> let the browser offer every option
_ATT = (os.environ.get("L2_PROBE_ATTACHMENT") or "").strip().lower()
ATTACHMENT = ({"platform": AuthenticatorAttachment.PLATFORM,
               "cross-platform": AuthenticatorAttachment.CROSS_PLATFORM}.get(_ATT))

_challenge = b""


# ---- inlined MDS + tier logic (mirrors the tested backend) ---------------

def _norm_aaguid(a: str) -> str:
    h = (a or "").replace("-", "").strip().lower()
    if len(h) != 32 or any(c not in "0123456789abcdef" for c in h) or h == "0" * 32:
        return ""
    return f"{h[0:8]}-{h[8:12]}-{h[12:16]}-{h[16:20]}-{h[20:32]}"


def _load_mds():
    if not MDS_PATH or not os.path.exists(MDS_PATH):
        return {}
    raw = open(MDS_PATH, encoding="utf-8").read().strip()
    if not raw.startswith("{"):
        parts = raw.split(".")
        raw = base64.urlsafe_b64decode(parts[1] + "=" * (-len(parts[1]) % 4)).decode()
    idx = {}
    for e in json.loads(raw).get("entries", []):
        k = _norm_aaguid(str(e.get("aaguid") or ""))
        if k:
            idx[k] = e
    return idx


_MDS = _load_mds()


def _mds_roots(aaguid):
    e = _MDS.get(_norm_aaguid(aaguid))
    if not e:
        return []
    out = []
    for b64 in (e.get("metadataStatement") or {}).get("attestationRootCertificates") or []:
        try:
            der = base64.b64decode(b64)
            out.append(b"-----BEGIN CERTIFICATE-----\n"
                       + base64.encodebytes(der) + b"-----END CERTIFICATE-----\n")
        except Exception:
            pass
    return out


def _x5c(attestation_object):
    try:
        import cbor2
        return list((cbor2.loads(attestation_object).get("attStmt") or {}).get("x5c") or [])
    except Exception:
        return []


def _describe(verified):
    aaguid = str(getattr(verified, "aaguid", "") or "")
    fmt = str(getattr(verified, "fmt", "none") or "none")
    attested = fmt not in ("none", "")
    in_mds = bool(_norm_aaguid(aaguid)) and _norm_aaguid(aaguid) in _MDS
    x5c = _x5c(getattr(verified, "attestation_object", b"") or b"")
    root_ok = False
    if attested and in_mds and x5c:
        roots = _mds_roots(aaguid)
        if roots:
            try:
                validate_certificate_chain(x5c=x5c, pem_root_certs_bytes=roots)
                root_ok = True
            except Exception:
                root_ok = False
    dev = str(getattr(verified, "credential_device_type", "") or "")
    tier = "l2_high" if (attested and in_mds and root_ok) else "l2_standard"
    return {
        "attestation_fmt": fmt, "aaguid": aaguid,
        "attestation_verified": attested, "aaguid_in_mds": in_mds,
        "attestation_root_verified": root_ok,
        "backup_eligible": dev == "multi_device",
        "backup_state": bool(getattr(verified, "credential_backed_up", False)),
        "user_verified": bool(getattr(verified, "user_verified", False)),
        "tier": tier,
    }


# ---- HTTP harness (identical page to the repo probe) ---------------------

PAGE = """<!doctype html><meta charset=utf-8>
<title>ProofKey L2 — device probe</title>
<style>body{font:15px system-ui;max-width:640px;margin:40px auto;padding:0 16px}
button{font:600 15px system-ui;padding:10px 18px;border-radius:8px;border:1px solid #888;cursor:pointer}
pre{background:#111;color:#e8e8ea;padding:14px;border-radius:8px;overflow:auto}
.row{margin:6px 0}.k{color:#666}.hi{color:#0a0}.lo{color:#b80}</style>
<h2>ProofKey L2 — device probe</h2>
<p>Register a passkey on this machine's authenticator and see its assurance tier.
Nothing is stored.</p>
<button onclick=probe()>Probe this device</button>
<pre id=out>ready.</pre>
<script>
const b64u = b => btoa(String.fromCharCode(...new Uint8Array(b))).replace(/\\+/g,'-').replace(/\\//g,'_').replace(/=+$/,'');
const unb64u = s => Uint8Array.from(atob(s.replace(/-/g,'+').replace(/_/g,'/')), c=>c.charCodeAt(0));
async function probe(){
  const out=document.getElementById('out'); out.textContent='requesting options…';
  const o=await (await fetch('/options')).json(); const pk=o.publicKey;
  pk.challenge=unb64u(pk.challenge); pk.user.id=unb64u(pk.user.id);
  if(pk.excludeCredentials) pk.excludeCredentials.forEach(c=>c.id=unb64u(c.id));
  out.textContent='touch your authenticator (Windows Hello / Touch ID / key / PIN)…';
  let cred; try{ cred=await navigator.credentials.create({publicKey:pk}); }
  catch(e){ out.textContent='cancelled / failed: '+e; return; }
  const body={id:cred.id,rawId:b64u(cred.rawId),type:cred.type,
    response:{attestationObject:b64u(cred.response.attestationObject),clientDataJSON:b64u(cred.response.clientDataJSON)},
    clientExtensionResults:cred.getClientExtensionResults()};
  const r=await (await fetch('/verify',{method:'POST',body:JSON.stringify(body)})).json();
  if(r.error){ out.textContent='verify failed: '+r.error; return; }
  const hi=r.tier==='l2_high';
  out.innerHTML=Object.entries(r).map(([k,v])=>
    `<div class=row><span class=k>${k.padEnd(24)}</span> <b class=${k==='tier'?(hi?'hi':'lo'):''}>${v}</b></div>`).join('');
}
</script>
"""


class H(http.server.BaseHTTPRequestHandler):
    def _send(self, code, body, ctype="application/json"):
        b = body if isinstance(body, bytes) else body.encode()
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(b)))
        self.end_headers()
        self.wfile.write(b)

    def do_GET(self):
        global _challenge
        if self.path == "/":
            return self._send(200, PAGE, "text/html; charset=utf-8")
        if self.path == "/options":
            opts = generate_registration_options(
                rp_id=RP_ID, rp_name="ProofKey L2 probe",
                user_name="probe@local", user_id=b"probe-user",
                attestation=AttestationConveyancePreference.DIRECT,
                authenticator_selection=AuthenticatorSelectionCriteria(
                    resident_key=ResidentKeyRequirement.PREFERRED,
                    user_verification=UserVerificationRequirement.REQUIRED,
                    authenticator_attachment=ATTACHMENT,
                ),
            )
            _challenge = opts.challenge
            return self._send(200, '{"publicKey":%s}' % options_to_json(opts))
        return self._send(404, '{"error":"not found"}')

    def do_POST(self):
        if self.path != "/verify":
            return self._send(404, '{"error":"not found"}')
        n = int(self.headers.get("Content-Length", 0))
        try:
            verified = verify_registration_response(
                credential=self.rfile.read(n).decode(), expected_challenge=_challenge,
                expected_rp_id=RP_ID, expected_origin=ORIGIN, require_user_verification=True,
            )
            row = _describe(verified)
        except Exception as e:  # noqa: BLE001
            return self._send(200, json.dumps({"error": str(e)}))
        print("\n=== device tier row ===")
        for k, v in row.items():
            print(f"  {k:24} {v}")
        if not MDS_PATH:
            print("  (FIDO_MDS_PATH unset -> aaguid_in_mds always False -> l2_standard)")
        print("=======================\n")
        return self._send(200, json.dumps(row))

    def log_message(self, *a):
        pass


if __name__ == "__main__":
    print(f"ProofKey L2 standalone device probe -> http://localhost:{PORT}")
    print(f"  RP id: {RP_ID}   MDS: {MDS_PATH or '(unset -> everything reads l2_standard)'}"
          f"   entries loaded: {len(_MDS)}")
    with socketserver.TCPServer(("127.0.0.1", PORT), H) as httpd:
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            print("\nbye")
