requests-schannel
Windows SChannel TLS/mTLS provider for requests and websockets — smartcard and PKI authentication via native Windows APIs.
requests-schannel replaces OpenSSL with the built-in Windows SChannel SSPI provider, enabling:
- Smartcard / PIV authentication — use certificates on hardware tokens without exporting private keys
- Windows certificate store integration — select client certificates by thumbprint, subject, or let Windows auto-select
- Native trust store — server verification uses the Windows trust store (no CA bundle files)
- TLS 1.2 / 1.3 with ALPN negotiation
- Zero native dependencies — pure-Python ctypes backend included; optional sspilib backend for enhanced performance
Table of Contents
- Installation
- Quick Start
- Usage
- Client Certificate (mTLS)
- Backends
- API Reference
- Configuration
- Error Handling
- Testing
- License
Installation
pip install requests-schannel
With optional dependencies:
# requests integration
pip install requests-schannel[requests]
# websockets integration
pip install requests-schannel[websockets]
# sspilib backend (recommended for production)
pip install requests-schannel[sspilib]
# Everything
pip install requests-schannel[all]
Note: This library requires Windows and Python 3.12+.
Quick Start
import requests
from requests_schannel import SchannelAdapter
session = requests.Session()
session.mount("https://", SchannelAdapter())
resp = session.get("https://example.com")
print(resp.status_code)
Usage
requests Integration
Drop-in Adapter
Mount SchannelAdapter onto a requests.Session to route all HTTPS traffic through SChannel:
import requests
from requests_schannel import SchannelAdapter
session = requests.Session()
session.mount("https://", SchannelAdapter())
resp = session.get("https://www.howsmyssl.com/a/check")
print(resp.json()["tls_version"])
Convenience Session Factory
create_session() returns a pre-configured session with the adapter already mounted:
from requests_schannel import create_session
session = create_session()
resp = session.get("https://example.com")
WebSocket Integration
Connect to WebSocket servers over TLS using SChannel:
import asyncio
from requests_schannel.ws import schannel_connect
async def main():
async with schannel_connect("wss://echo.websocket.org") as ws:
await ws.send("hello")
response = await ws.recv()
print(response)
asyncio.run(main())
Low-Level Socket API
Use SchannelContext and SchannelSocket directly for custom TLS connections:
import socket
from requests_schannel import SchannelContext
ctx = SchannelContext()
ctx.set_alpn_protocols(["http/1.1"])
raw_sock = socket.create_connection(("example.com", 443))
tls_sock = ctx.wrap_socket(raw_sock, server_hostname="example.com")
tls_sock.send(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
print(tls_sock.recv(4096))
tls_sock.close()
Async Socket API
For asyncio applications that need TLS without requests or websockets:
import asyncio
from requests_schannel import SchannelContext, AsyncSchannelSocket
async def main():
ctx = SchannelContext()
sock = await AsyncSchannelSocket.connect("example.com", 443, ctx)
await sock.send(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
data = await sock.recv(4096)
print(data)
await sock.close()
asyncio.run(main())
Client Certificate (mTLS)
Select a client certificate from the Windows certificate store for mutual TLS authentication.
By Thumbprint
from requests_schannel import SchannelAdapter, create_session
# Using the adapter directly
adapter = SchannelAdapter(client_cert_thumbprint="AB12CD34EF56...")
session = requests.Session()
session.mount("https://", adapter)
# Or using the convenience factory
session = create_session(client_cert_thumbprint="AB12CD34EF56...")
By Subject Name
session = create_session(client_cert_subject="CN=myuser")
Auto-Select
Let Windows choose a suitable client certificate (may display a UI prompt):
session = create_session(auto_select_client_cert=True)
Custom Certificate Store
By default, certificates are loaded from the "MY" (Personal) store. Specify a different store:
session = create_session(
client_cert_thumbprint="AB12CD34EF56...",
cert_store_name="MY", # "MY", "Root", "CA", etc.
)
Window Handle (hwnd) for UI Dialogs
When a client certificate requires user interaction (e.g. smartcard PIN prompt or certificate selection dialog), Windows shows a security dialog. By default this dialog may appear behind the application window. Pass a parent window handle (hwnd) to ensure it appears on top:
import ctypes
# Get the foreground window handle
hwnd = ctypes.windll.user32.GetForegroundWindow()
session = create_session(
client_cert_thumbprint="AB12CD34EF56...",
hwnd=hwnd,
)
With a tkinter application:
import tkinter as tk
app = tk.Tk()
session = create_session(
client_cert_thumbprint="AB12CD34EF56...",
hwnd=app.winfo_id(),
)
Or directly on the context:
ctx = SchannelContext()
ctx.client_cert_thumbprint = "AB12CD34EF56..."
ctx.hwnd = ctypes.windll.user32.GetForegroundWindow()
WebSocket mTLS
async with schannel_connect(
"wss://secure.example.com/ws",
client_cert_thumbprint="AB12CD34EF56...",
) as ws:
await ws.send("authenticated!")
Backends
requests-schannel provides two backends for interacting with the Windows SChannel API:
| Backend | Description | Install |
|---|---|---|
| ctypes | Pure-Python ctypes calls to secur32.dll / crypt32.dll. Zero dependencies. Ships with the package. |
Built-in |
| sspilib | Uses the sspilib package for SSPI operations. Recommended for production. | pip install requests-schannel[sspilib] |
The backend is auto-selected (sspilib if available, otherwise ctypes). To force a specific backend:
from requests_schannel import SchannelAdapter, SchannelContext
# Via adapter
adapter = SchannelAdapter(backend="ctypes")
# Via context
ctx = SchannelContext(backend="sspilib")
API Reference
SchannelAdapter
requests.adapters.HTTPAdapter subclass for SChannel TLS.
SchannelAdapter(
client_cert_thumbprint: str | None = None,
client_cert_subject: str | None = None,
auto_select_client_cert: bool = False,
cert_store_name: str = "MY",
alpn_protocols: list[str] | None = None,
backend: str | None = None,
schannel_context: SchannelContext | None = None,
hwnd: int | None = None,
**kwargs, # passed to HTTPAdapter
)
Properties:
schannel_context— the underlyingSchannelContextinstance
create_session()
Convenience factory that returns a requests.Session with SchannelAdapter mounted on https://.
create_session(
client_cert_thumbprint: str | None = None,
client_cert_subject: str | None = None,
auto_select_client_cert: bool = False,
cert_store_name: str = "MY",
alpn_protocols: list[str] | None = None,
backend: str | None = None,
hwnd: int | None = None,
**kwargs,
) -> requests.Session
SchannelContext
ssl.SSLContext-compatible object backed by Windows SChannel. Thread-safe — the credential handle is shared; each wrap_socket() creates a new per-connection security context.
SchannelContext(backend: str | SchannelBackend | None = None)
Properties:
client_cert_thumbprint— SHA-1 thumbprint for mTLS client cert selectionclient_cert_subject— subject name substring for client cert selectionauto_select_client_cert— let Windows auto-select a client certcert_store_name— Windows certificate store name (default"MY")hwnd— parent window handle (HWND) for Windows Security dialogs so they appear on top of the application windowminimum_version/maximum_version—TlsVersion.TLSv1_2orTlsVersion.TLSv1_3verify_mode—ssl.CERT_REQUIRED(default) orssl.CERT_NONEcheck_hostname— enable/disable hostname verification
Methods:
set_alpn_protocols(protocols: list[str])— set ALPN protocol list (e.g.["h2", "http/1.1"])wrap_socket(sock, server_hostname=..., do_handshake_on_connect=True) -> SchannelSocketload_cert_chain(),load_verify_locations(),set_ciphers()— no-ops forssl.SSLContextcompatibility
SchannelSocket
TLS-wrapped socket with an ssl.SSLSocket-compatible interface.
Methods:
do_handshake()— perform the TLS handshakesend(data: bytes) -> int/write(data: bytes) -> intrecv(bufsize: int) -> bytes/read(nbytes: int) -> bytesrecv_into(buffer, nbytes) -> intgetpeercert(binary_form=False)— get peer certificateselected_alpn_protocol() -> str | Noneversion() -> str | None— negotiated TLS versionclose()— send close_notify and close the connectionmakefile(mode, buffering)— create a file-like wrapper (for urllib3 compatibility)
AsyncSchannelSocket
Async wrapper around SchannelSocket for asyncio.
# Connect and handshake
sock = await AsyncSchannelSocket.connect(host, port, context)
# Wrap existing socket
sock = await AsyncSchannelSocket.wrap(raw_sock, context, server_hostname)
# I/O
await sock.send(data)
data = await sock.recv(4096)
await sock.close()
schannel_connect()
Async context manager for WebSocket connections over SChannel TLS.
async with schannel_connect(
uri: str,
*,
context: SchannelContext | None = None,
client_cert_thumbprint: str | None = None,
client_cert_subject: str | None = None,
auto_select_client_cert: bool = False,
cert_store_name: str = "MY",
backend: str | None = None,
timeout: float | None = 30.0,
additional_headers: dict[str, str] | None = None,
**ws_kwargs,
) as ws:
...
Data Classes
TlsVersion— enum:TLSv1_2,TLSv1_3CertInfo— certificate metadata:thumbprint,subject,issuer,friendly_name,not_before,not_after,has_private_key,serial_number,der_encodedConnectionInfo— TLS connection details:protocol_version,cipher_algorithm,cipher_strength,hash_algorithm,hash_strength,exchange_algorithm,exchange_strengthStreamSizes— SChannel buffer sizes:header,trailer,max_message,buffers,block_size
Configuration
TLS Version
from requests_schannel import SchannelContext, TlsVersion
ctx = SchannelContext()
ctx.minimum_version = TlsVersion.TLSv1_2 # default
ctx.maximum_version = TlsVersion.TLSv1_3 # default
ALPN Protocols
ctx = SchannelContext()
ctx.set_alpn_protocols(["h2", "http/1.1"])
Disable Server Verification
Warning: Only use this for testing/development.
import ssl
ctx = SchannelContext()
ctx.verify_mode = ssl.CERT_NONE
ctx.check_hostname = False
Error Handling
All exceptions inherit from SchannelError:
| Exception | Description |
|---|---|
SchannelError |
Base exception for all SChannel errors |
HandshakeError |
TLS handshake failed |
CertificateError |
Base for certificate-related errors |
CertificateNotFoundError |
Certificate not found in the Windows store |
CertificateExpiredError |
Certificate has expired |
CertificateUntrustedError |
Certificate chain is not trusted |
CertificateVerificationError |
Server certificate verification failed |
CredentialError |
Failed to acquire SSPI credentials |
DecryptionError |
Failed to decrypt incoming TLS data |
EncryptionError |
Failed to encrypt outgoing TLS data |
BackendError |
Backend (sspilib or ctypes) is unavailable |
ContextExpiredError |
Security context has expired |
RenegotiationError |
TLS renegotiation failed |
from requests_schannel._errors import SchannelError, CertificateNotFoundError
try:
session = create_session(client_cert_thumbprint="INVALID...")
session.get("https://secure.example.com")
except CertificateNotFoundError:
print("Certificate not found in store")
except SchannelError as e:
print(f"SChannel error: {e}")
Testing
# Install dev dependencies
pip install -e ".[test]"
# Run unit tests
pytest tests/unit/
# Run integration tests (requires Windows with network access)
pytest tests/integration/
# Run all tests with coverage
pytest --cov=requests_schannel --cov-report=term-missing
License
Metadata
Release files for requests-schannel 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| requests_schannel-0.2.0.tar.gz | 111.9 kB | With provenance | Details for requests_schannel-0.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| requests_schannel-0.2.0-py3-none-any.whl | Python 3 | none | any | Details for requests_schannel-0.2.0-py3-none-any.whl |
Total release size: 149.9 kB
Release files / requests_schannel-0.2.0.tar.gz
| Download URL | requests_schannel-0.2.0.tar.gz |
|---|---|
| Size | 111.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f08e6e175884759d0db976209457f238580d5db1d99730e1543f51066e9a4065
|
|
BLAKE2b-256 checksum How to use checksums |
bbf9583c3e88440808fce7560590c76d77eb9b6a082be334f33440ce6118b7b6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 25, 2026.
Transparency logRelease files / requests_schannel-0.2.0-py3-none-any.whl
| Download URL | requests_schannel-0.2.0-py3-none-any.whl |
|---|---|
| Size | 38.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
95a9d81e710e5cfeeeace426bd126383ef6b02e631047d54cc9d6824e33c3b79
|
|
BLAKE2b-256 checksum How to use checksums |
381bcd103999cc88871a683f96dc58adb48850903a263cb1e0ec0d252fe64f7f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 25, 2026.
Transparency logSecurity for requests-schannel 0.2.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for requests_schannel-0.2.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.