This commit is contained in:
@@ -25,7 +25,7 @@ def _log(addr:tuple, command:str, profile:str|None, status:str, error:str|None=N
|
||||
else:
|
||||
console.print(f"[dim]{ts}[/dim] {addr_str} {profile_str}[cyan]{command}[/cyan] [green]{status}[/green]")
|
||||
|
||||
def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth_keys:list[str]|None, auth_keys_path:"Path|None", nonce:str) -> None:
|
||||
def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth_keys:list[str]|None, auth_keys_path:"Path|None", nonce:str, pq_private_key=None) -> None:
|
||||
from browser_cli.client import _resolve_socket, BrowserNotConnected
|
||||
from browser_cli.platform import is_windows
|
||||
|
||||
@@ -92,8 +92,26 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
_send_error(msg_id, "unauthorized: untrusted public key")
|
||||
_log(addr, command, None, "DENIED", "untrusted key")
|
||||
return
|
||||
pq_shared_secret = None
|
||||
if pq_private_key is not None:
|
||||
kex = msg.get("pq_kex") or {}
|
||||
pq_required = parse_version(client_ver) >= parse_version("0.9.4")
|
||||
if not isinstance(kex, dict) or kex.get("alg") != "ML-KEM-768" or not kex.get("ciphertext"):
|
||||
if pq_required:
|
||||
_send_error(msg_id, "unauthorized: post-quantum key exchange required")
|
||||
_log(addr, command, None, "DENIED", "missing pq kex")
|
||||
return
|
||||
else:
|
||||
try:
|
||||
from browser_cli.auth import pq_kex_server_decapsulate
|
||||
pq_shared_secret = pq_kex_server_decapsulate(pq_private_key, str(kex["ciphertext"]))
|
||||
except Exception:
|
||||
_send_error(msg_id, "unauthorized: invalid post-quantum key exchange")
|
||||
_log(addr, command, None, "DENIED", "bad pq kex")
|
||||
return
|
||||
|
||||
from browser_cli.auth import verify
|
||||
if not verify(pub, bytes.fromhex(nonce), msg, sig):
|
||||
if not verify(pub, bytes.fromhex(nonce), msg, sig, pq_shared_secret):
|
||||
_send_error(msg_id, "unauthorized: invalid signature")
|
||||
_log(addr, command, None, "DENIED", "bad signature")
|
||||
return
|
||||
@@ -140,7 +158,7 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
resolved_profile = msg.get("_route") or profile
|
||||
|
||||
# ── strip protocol fields, apply request compat shim, forward ─────────────
|
||||
strip = {"token", "_route", "pubkey", "sig", "user_agent"}
|
||||
strip = {"token", "_route", "pubkey", "sig", "user_agent", "pq_kex"}
|
||||
clean_msg = {k: v for k, v in msg.items() if k not in strip}
|
||||
clean_msg = adapt_request(clean_msg, client_ver)
|
||||
clean_payload = json.dumps(clean_msg).encode()
|
||||
@@ -192,17 +210,25 @@ def _handle_client(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
else:
|
||||
auth_keys = None
|
||||
nonce = secrets.token_hex(32)
|
||||
challenge = json.dumps({
|
||||
pq_private_key = None
|
||||
challenge_msg = {
|
||||
"type": "challenge",
|
||||
"nonce": nonce,
|
||||
"server_version": get_installed_version(),
|
||||
"min_client_version": PROTOCOL_MIN_CLIENT,
|
||||
}).encode()
|
||||
}
|
||||
if auth_keys_path is not None:
|
||||
from browser_cli.auth import PQ_KEX_ALG, pq_kex_server_keypair
|
||||
pq_keypair = pq_kex_server_keypair()
|
||||
if pq_keypair is not None:
|
||||
pq_private_key, pq_public_key = pq_keypair
|
||||
challenge_msg["pq_kex"] = {"alg": PQ_KEX_ALG, "public_key": pq_public_key.hex()}
|
||||
challenge = json.dumps(challenge_msg).encode()
|
||||
try:
|
||||
_framed_send(client_sock, challenge)
|
||||
except OSError:
|
||||
return
|
||||
_proxy_request(client_sock, addr, profile, auth_keys, auth_keys_path, nonce)
|
||||
_proxy_request(client_sock, addr, profile, auth_keys, auth_keys_path, nonce, pq_private_key)
|
||||
finally:
|
||||
_CONN_LIMIT.release()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user