Extend SPEC §16 requirements 5–6 (dial-time address judgement of the advertised issuer, token_endpoint, and device_authorization_endpoint) to the Python SDK. Appendix F records today's state: scheme gate, bounded timeout, follow_redirects=False, bounded body — a private address is still dialed.
Step 0 (upstream, blocking): a Python surfguard classification-core port in the surfguard repo (classification + resolve, IANA generator over script/iana/*.json, mirrored 157-case corpus with byte-equality drift tests). Tracked as basecamp/surfguard#25.
Enforcement design and the open question to settle first:
- The seam is a custom
httpx transport over a resolving httpcore backend: resolve the hostname, judge every resolved address, and connect to exactly one judged address (pin), keeping the hostname for SNI/Host — connect-time judgement with no second resolution, which is the DNS-rebinding requirement (the address judged MUST be the address connected).
- The design question: httpx/httpcore has no first-class dial hook; the options are a custom
httpcore ConnectionPool with an overridden origin resolver, or constructing requests against the pinned IP with an explicit Host header + SNI override (ssl.SSLContext wrapping). The chosen mechanism must hold TLS verification against the original hostname. Prototype before speccing the final shape.
- The OAuth module currently uses module-level
httpx.post/httpx.stream calls (exchange) and client construction inside the device/discovery paths — enforcement lands by routing all of these through one policed client/transport factory, which is also where the override surface attaches.
- Loopback is explicit operator configuration: a policy option on the OAuth entry points (mirroring Go's
AllowLoopback), never inferred from the endpoint URL or other request-derived values. The existing is_localhost HTTPS exemption is a scheme judgement, not address admission.
- Overrides (SPEC §16 contract): replacement policy, replacement client/transport, disablement (a caller-supplied client counts — "yours, enforcement included").
- Verdicts:
invalid_issuer_origin on the discovery hop, api_error elsewhere, non-retryable, device poll terminates on first refusal; unresolvable stays a retryable network fault.
Definition of done: Appendix F rows updated; unit tests assert refusal-before-dial for private/loopback/CGNAT/special-purpose targets on discovery, device, and exchange paths, plus override and injected-client contracts.
Extend SPEC §16 requirements 5–6 (dial-time address judgement of the advertised issuer,
token_endpoint, anddevice_authorization_endpoint) to the Python SDK. Appendix F records today's state: scheme gate, bounded timeout,follow_redirects=False, bounded body — a private address is still dialed.Step 0 (upstream, blocking): a Python surfguard classification-core port in the surfguard repo (classification + resolve, IANA generator over
script/iana/*.json, mirrored 157-case corpus with byte-equality drift tests). Tracked as basecamp/surfguard#25.Enforcement design and the open question to settle first:
httpxtransport over a resolvinghttpcorebackend: resolve the hostname, judge every resolved address, and connect to exactly one judged address (pin), keeping the hostname for SNI/Host — connect-time judgement with no second resolution, which is the DNS-rebinding requirement (the address judged MUST be the address connected).httpcoreConnectionPoolwith an overridden origin resolver, or constructing requests against the pinned IP with an explicitHostheader + SNI override (ssl.SSLContextwrapping). The chosen mechanism must hold TLS verification against the original hostname. Prototype before speccing the final shape.httpx.post/httpx.streamcalls (exchange) and client construction inside the device/discovery paths — enforcement lands by routing all of these through one policed client/transport factory, which is also where the override surface attaches.AllowLoopback), never inferred from the endpoint URL or other request-derived values. The existingis_localhostHTTPS exemption is a scheme judgement, not address admission.invalid_issuer_originon the discovery hop,api_errorelsewhere, non-retryable, device poll terminates on first refusal; unresolvable stays a retryable network fault.Definition of done: Appendix F rows updated; unit tests assert refusal-before-dial for private/loopback/CGNAT/special-purpose targets on discovery, device, and exchange paths, plus override and injected-client contracts.