fix(auth): sign_request strips URL fragment + PSS config module-cache#371
Conversation
sign_request previously stripped the query string but left any '#fragment' intact. httpx drops fragments from the request-target before transmission (RFC 3986 §3.5), so the SDK was signing /path#frag while httpx sent /path — a guaranteed signature mismatch surfacing as a 401 KalshiAuthError with no hint that a stray '#' was the cause. The canonicalization now applies the same shape as the existing '?' strip. The RSA-PSS / MGF1 / SHA256 configuration objects are pure immutable algorithm descriptors. They were being reallocated on every signature; for a high-RPS client the per-call Python-object churn is wasted work. They are now bound once at module import as _SHA256 / _PSS_PADDING and reused. The cryptography library explicitly supports reuse of these instances. Regression tests verify (a) signatures for fragment-bearing paths verify against the fragment-stripped canonical message — the right oracle since PSS uses a random salt and is non-deterministic — and (b) the cached config objects retain identity across sign_request calls. Closes #317, #345
Code ReviewOverviewTwo surgical fixes in Fix #317 — Fragment stripping# Before
clean_path = path.split("?")[0]
# After
clean_path = path.split("?", 1)[0].split("#", 1)[0]Correct. The chained split handles all cases:
Adding Edge case not tested (low severity): A path that is only Fix #345 — Module-level PSS/SHA256 caching_SHA256 = hashes.SHA256()
_PSS_PADDING = padding.PSS(mgf=padding.MGF1(_SHA256), salt_length=padding.PSS.DIGEST_LENGTH)Correct. One suggestion: Since the code already has the threading context documented elsewhere and # Module-level cached config for RSA-PSS signing (#345). Both objects are
# immutable descriptors (no mutable state); safe to share across threads.This removes any future doubt for someone who sees the executor in Tests
Minor style noteCLAUDE.md says "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." The new module-level comment is 3 lines. The existing file has similar 3-line comment blocks throughout, so this is consistent in practice even if technically over the limit. Not a blocker. VerdictApprove. Both fixes are correct, minimal, and well-tested. The fragment-stripping logic handles every edge case including malformed URLs. The PSS caching is a legitimate performance improvement with no correctness risk. The only actionable suggestion is adding a thread-safety note to the module-level comment for future maintainers. |
Summary
Two surgical fixes in
kalshi/auth.py:sign_requestnow strips#fragmentfrom the path before signing (httpx drops fragments before transmission, so signing them produced a guaranteed signature mismatch surfacing as a 401), and the immutable RSA-PSS / MGF1 / SHA256 algorithm descriptors are hoisted to module-level singletons so they are no longer reallocated on every request and websocket reconnect.Issues closed
sign_requeststrips query string but not URL fragment, producing 401 on#-bearing paths.Behavioral changes
#fragmentare now canonicalized the same way httpx canonicalizes them on the wire: the fragment is stripped before the signing payload is built. Previously such paths produced a 401KalshiAuthError; they now succeed.Tests
tests/test_auth.py::TestIssueRegressions::test_issue_317_sign_request_strips_url_fragment— verifies that signatures produced for/path#frag,/path?q=x#frag, and/path#all verify against the fragment-stripped canonical message (RSA-PSS is non-deterministic, so verification, not byte equality, is the right oracle).tests/test_auth.py::TestIssueRegressions::test_issue_345_pss_config_cached_at_module_level— asserts_SHA256/_PSS_PADDINGexist at module scope and are not re-bound acrosssign_requestcalls, and that the cached config still produces a verifiable signature.Source
Round-3 independent audit closure plan, wave W3.