diff --git a/prototype/gate_store.py b/prototype/gate_store.py index 1c5d5fa0..b7bb2e7d 100644 --- a/prototype/gate_store.py +++ b/prototype/gate_store.py @@ -20,6 +20,7 @@ import base64 import hmac import json +import math import os import re import socket @@ -309,6 +310,13 @@ def _no_constants(token: str) -> Any: raise ValueError(f"non-finite json constant: {token}") +def _finite_float(token: str) -> float: + val = float(token) + if not math.isfinite(val): + raise ValueError(f"non-finite json float: {token}") + return val + + def _send(sock: socket.socket, message: dict[str, Any]) -> None: body = json.dumps(message, separators=(",", ":"), allow_nan=False).encode() sock.sendall(struct.pack(">I", len(body)) + body) @@ -331,14 +339,17 @@ def _recv(sock: socket.socket, limit: int) -> Any: raw = _recv_exact(sock, size) try: return json.loads( - raw.decode("utf-8"), object_pairs_hook=_pairs, parse_constant=_no_constants + raw.decode("utf-8"), + object_pairs_hook=_pairs, + parse_constant=_no_constants, + parse_float=_finite_float, ) except RecursionError as exc: raise ValueError("malformed json") from exc def _str(value: object) -> str: - if not isinstance(value, str): + if not isinstance(value, str) or not value: raise TypeError("string field") return value @@ -353,6 +364,8 @@ def _digest(value: object) -> str: def _int(value: object) -> int: if isinstance(value, bool) or not isinstance(value, int): raise TypeError("integer field") + if not 0 <= value <= 9_007_199_254_740_991: + raise ValueError("integer field out of range") return value @@ -465,6 +478,13 @@ class RemoteGateStore: def __init__(self, host: str, port: int, secret: bytes, *, timeout: float = 5.0) -> None: if type(secret) is not bytes or len(secret) < 16: raise ValueError("store secret must be at least 16 bytes") + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(timeout) + or timeout <= 0 + ): + raise ValueError("timeout must be a positive finite number") self.address = (host, port) self._secret = _b64(secret) self._timeout = timeout diff --git a/tests/test_gate_store_and_cmcp_gate_hardening.py b/tests/test_gate_store_and_cmcp_gate_hardening.py index 7b275e19..27147cf2 100644 --- a/tests/test_gate_store_and_cmcp_gate_hardening.py +++ b/tests/test_gate_store_and_cmcp_gate_hardening.py @@ -429,3 +429,69 @@ def test_cmcp_trace_gate_recheck_and_receipt_validate_handle_and_reason( handle = CallAdmission(raw, b"{}", 1, "session-1", "call-1") with pytest.raises(v.ProfileError, match="^input_invalid$"): call(gate, handle) + + +@pytest.mark.parametrize("raw_frame", [b'{"ok":1e309}', b'{"ok":-1e309}']) +def test_recv_rejects_float_exponent_overflow(raw_frame: bytes) -> None: + left, right = socket.socketpair() + try: + frame = struct.pack(">I", len(raw_frame)) + raw_frame + sender = threading.Thread(target=left.sendall, args=(frame,), daemon=True) + sender.start() + with pytest.raises(ValueError, match="non-finite json float"): + gate_store._recv(right, 1 << 20) + sender.join(timeout=5) + finally: + left.close() + right.close() + + +@pytest.mark.parametrize( + "bad_val", + [-1, 9_007_199_254_740_992, 1 << 64], +) +def test_int_and_server_reject_out_of_range_integers_without_crashing( + bad_val: int, tmp_path: Path +) -> None: + assert gate_store._int(0) == 0 + assert gate_store._int(9_007_199_254_740_991) == 9_007_199_254_740_991 + with pytest.raises(ValueError, match="out of range"): + gate_store._int(bad_val) + + with _running_server(tmp_path / "int-bounds.sqlite", BINARY_SECRET) as server: + port = int(server.server_address[1]) + store = RemoteGateStore("127.0.0.1", port, BINARY_SECRET) + with pytest.raises(GateStoreUnavailable, match="malformed"): + store.mint_challenge( + issuer="iss", + token_id="jti", + token_digest="sha256:" + "0" * 64, + nonce="n" * 43, + session_id="s1", + action_digest="sha256:" + "1" * 64, + issued=bad_val, + expires=bad_val + 10 if bad_val >= 0 else 10, + ) + + +def test_str_and_server_reject_empty_string_identifiers(tmp_path: Path) -> None: + assert gate_store._str("ok") == "ok" + with pytest.raises(TypeError, match="string field"): + gate_store._str("") + + with _running_server(tmp_path / "empty-str.sqlite", BINARY_SECRET) as server: + port = int(server.server_address[1]) + store = RemoteGateStore("127.0.0.1", port, BINARY_SECRET) + with pytest.raises(GateStoreUnavailable, match="malformed"): + store.admission("") + + +@pytest.mark.parametrize( + "bad_timeout", + [False, True, 0, -1.0, float("nan"), float("inf"), "5"], +) +def test_remote_gate_store_rejects_invalid_timeout(bad_timeout: Any) -> None: + assert RemoteGateStore("127.0.0.1", 9999, BINARY_SECRET, timeout=2.5)._timeout == 2.5 + with pytest.raises(ValueError, match="^timeout must be a positive finite number$"): + RemoteGateStore("127.0.0.1", 9999, BINARY_SECRET, timeout=bad_timeout) +