sslcontext 1.4.3__tar.gz → 2.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. sslcontext-2.1.0/.gitignore +37 -0
  2. sslcontext-2.1.0/CHANGELOG.md +97 -0
  3. sslcontext-2.1.0/LICENSE +21 -0
  4. sslcontext-2.1.0/PKG-INFO +168 -0
  5. sslcontext-2.1.0/README.md +136 -0
  6. sslcontext-2.1.0/docs/api/reference.md +41 -0
  7. sslcontext-2.1.0/docs/changelog.md +3 -0
  8. sslcontext-2.1.0/docs/header.md +7 -0
  9. sslcontext-2.1.0/docs/index.md +59 -0
  10. sslcontext-2.1.0/mkdocs.yml +52 -0
  11. sslcontext-2.1.0/pyproject.toml +59 -0
  12. sslcontext-2.1.0/src/sslcontext/AGENTS.md +644 -0
  13. sslcontext-2.1.0/src/sslcontext/__init__.py +80 -0
  14. sslcontext-2.1.0/src/sslcontext/__main__.py +5 -0
  15. sslcontext-2.1.0/src/sslcontext/_asn1.py +118 -0
  16. sslcontext-2.1.0/src/sslcontext/_context.py +106 -0
  17. sslcontext-2.1.0/src/sslcontext/_identity.py +112 -0
  18. sslcontext-2.1.0/src/sslcontext/_load.py +241 -0
  19. sslcontext-2.1.0/src/sslcontext/_openssl/__init__.py +8 -0
  20. sslcontext-2.1.0/src/sslcontext/_openssl/_api.py +261 -0
  21. sslcontext-2.1.0/src/sslcontext/_openssl/_certcb.py +219 -0
  22. sslcontext-2.1.0/src/sslcontext/_openssl/_ctx.py +58 -0
  23. sslcontext-2.1.0/src/sslcontext/_openssl/_engine.py +268 -0
  24. sslcontext-2.1.0/src/sslcontext/_openssl/_errors.py +22 -0
  25. sslcontext-2.1.0/src/sslcontext/_openssl/_keys.py +77 -0
  26. sslcontext-2.1.0/src/sslcontext/_openssl/_loader.py +152 -0
  27. sslcontext-2.1.0/src/sslcontext/_openssl/_objects.py +225 -0
  28. sslcontext-2.1.0/src/sslcontext/_openssl/_params.py +134 -0
  29. sslcontext-2.1.0/src/sslcontext/_openssl/_provider.py +692 -0
  30. sslcontext-2.1.0/src/sslcontext/_openssl/_signing.py +43 -0
  31. sslcontext-2.1.0/src/sslcontext/_pin.py +126 -0
  32. sslcontext-2.1.0/src/sslcontext/_policy.py +158 -0
  33. sslcontext-2.1.0/src/sslcontext/_select.py +448 -0
  34. sslcontext-2.1.0/src/sslcontext/_sources.py +99 -0
  35. sslcontext-2.1.0/src/sslcontext/_system.py +188 -0
  36. sslcontext-2.1.0/src/sslcontext/backends/__init__.py +189 -0
  37. sslcontext-2.1.0/src/sslcontext/backends/_cng.py +549 -0
  38. sslcontext-2.1.0/src/sslcontext/backends/_cryptoki.py +348 -0
  39. sslcontext-2.1.0/src/sslcontext/backends/_keychain.py +341 -0
  40. sslcontext-2.1.0/src/sslcontext/backends/_macos.py +257 -0
  41. sslcontext-2.1.0/src/sslcontext/backends/_pkcs11.py +406 -0
  42. sslcontext-2.1.0/src/sslcontext/cli/__init__.py +36 -0
  43. sslcontext-2.1.0/src/sslcontext/cli/_common.py +113 -0
  44. sslcontext-2.1.0/src/sslcontext/cli/_keystore.py +99 -0
  45. sslcontext-2.1.0/src/sslcontext/cli/_probe.py +166 -0
  46. sslcontext-2.1.0/src/sslcontext/cli/_root.py +30 -0
  47. sslcontext-2.1.0/src/sslcontext/exceptions.py +45 -0
  48. sslcontext-2.1.0/src/sslcontext/issuing.py +421 -0
  49. sslcontext-2.1.0/src/sslcontext/testing.py +154 -0
  50. sslcontext-2.1.0/tests/conftest.py +148 -0
  51. sslcontext-2.1.0/tests/data/ca.pem +11 -0
  52. sslcontext-2.1.0/tests/data/client-ec.der +0 -0
  53. sslcontext-2.1.0/tests/data/client-ec.enc.der +0 -0
  54. sslcontext-2.1.0/tests/data/client-ec.enc.key +8 -0
  55. sslcontext-2.1.0/tests/data/client-ec.key +5 -0
  56. sslcontext-2.1.0/tests/data/client-ec.pem +11 -0
  57. sslcontext-2.1.0/tests/data/client-rsa.key +28 -0
  58. sslcontext-2.1.0/tests/data/client-rsa.p12 +0 -0
  59. sslcontext-2.1.0/tests/data/client-rsa.pem +16 -0
  60. sslcontext-2.1.0/tests/data/server.key +28 -0
  61. sslcontext-2.1.0/tests/data/server.pem +16 -0
  62. sslcontext-2.1.0/tests/integration/start_virtual_card.sh +27 -0
  63. sslcontext-2.1.0/tests/integration/test_cng.py +189 -0
  64. sslcontext-2.1.0/tests/integration/test_keychain.py +126 -0
  65. sslcontext-2.1.0/tests/integration/test_pkcs11.py +350 -0
  66. sslcontext-2.1.0/tests/test_backends.py +71 -0
  67. sslcontext-2.1.0/tests/test_cli.py +120 -0
  68. sslcontext-2.1.0/tests/test_context.py +110 -0
  69. sslcontext-2.1.0/tests/test_issuing.py +102 -0
  70. sslcontext-2.1.0/tests/test_load.py +86 -0
  71. sslcontext-2.1.0/tests/test_package.py +5 -0
  72. sslcontext-2.1.0/tests/test_pin.py +90 -0
  73. sslcontext-2.1.0/tests/test_select.py +415 -0
  74. sslcontext-2.1.0/tests/test_signer.py +163 -0
  75. sslcontext-2.1.0/tests/test_sources.py +246 -0
  76. sslcontext-1.4.3/PKG-INFO +0 -15
  77. sslcontext-1.4.3/setup.cfg +0 -4
  78. sslcontext-1.4.3/setup.py +0 -89
  79. sslcontext-1.4.3/src/sslcontext/__init__.py +0 -13
  80. sslcontext-1.4.3/src/sslcontext/_vendor/_utils.py +0 -8
  81. sslcontext-1.4.3/src/sslcontext/_vendor/imports.py +0 -8
  82. sslcontext-1.4.3/src/sslcontext/_vendor/pyopenssl.py +0 -418
  83. sslcontext-1.4.3/src/sslcontext/_vendor/ssl.py +0 -81
  84. sslcontext-1.4.3/src/sslcontext/_vendor/wait.py +0 -115
  85. sslcontext-1.4.3/src/sslcontext/interface.py +0 -169
  86. sslcontext-1.4.3/src/sslcontext/utils.py +0 -50
  87. sslcontext-1.4.3/src/sslcontext.egg-info/PKG-INFO +0 -15
  88. sslcontext-1.4.3/src/sslcontext.egg-info/SOURCES.txt +0 -15
  89. sslcontext-1.4.3/src/sslcontext.egg-info/dependency_links.txt +0 -1
  90. sslcontext-1.4.3/src/sslcontext.egg-info/requires.txt +0 -9
  91. sslcontext-1.4.3/src/sslcontext.egg-info/top_level.txt +0 -1
  92. /sslcontext-1.4.3/src/sslcontext/_vendor/__init__.py → /sslcontext-2.1.0/src/sslcontext/py.typed +0 -0
@@ -0,0 +1,37 @@
1
+ # Category rule: nothing dot-prefixed at the repo root gets tracked except the
2
+ # git config files and the CI workflows re-included below.
3
+ /.*
4
+ !/.gitignore
5
+ !/.gitattributes
6
+ !/.github/
7
+
8
+ # Private agent config. No trailing slash: .agents may be a symlink.
9
+ # *.local.* is the unshared override and is never committed.
10
+ .agents
11
+ *.local.*
12
+ CLAUDE*
13
+ .claude
14
+
15
+ # Build/dist output and generated docs site
16
+ build/
17
+ dist/
18
+ site/
19
+
20
+ # Python
21
+ __pycache__/
22
+ *.py[cod]
23
+ .pytest_cache/
24
+ .hypothesis/
25
+ .mypy_cache/
26
+ .ruff_cache/
27
+ .coverage*
28
+ htmlcov/
29
+ *.egg-info/
30
+ .pyvenv/
31
+ .venv/
32
+
33
+ # OS/editor
34
+ .DS_Store
35
+ Thumbs.db
36
+ .idea/
37
+ .vscode/
@@ -0,0 +1,97 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ Nothing yet.
11
+
12
+ ## [2.1.0] - 2026-10-19
13
+
14
+ ### Added
15
+
16
+ - A PKCS#12 bundle (`.p12`/`.pfx`) is accepted wherever a certificate is
17
+ (`certificate=`, `load_pem()`), decrypted with `password=`; encrypted DER
18
+ (PKCS#8) private keys are decrypted with `password=` as PEM ones are.
19
+ - A password may be `bytes`, or a file holding it (a path object, or anything
20
+ with `open()` or `read()`; its first line), read only when needed.
21
+
22
+ ## [2.0.0] - 2026-10-19
23
+
24
+ ### Added
25
+
26
+ - The `sslcontext` command (extra `cli`): `list` and `show` keystore
27
+ identities, `probe URL` to see a server's certificate request, the
28
+ candidates and the certificate sent.
29
+ - `KeychainBackend` (`sslcontext.backends`, name `keychain`): identities in
30
+ the macOS Keychain and on smart cards, signed silently or, with
31
+ `KEYSTORE_PROMPT`, after macOS's own dialog. `system_sources()` uses it on
32
+ macOS.
33
+ - On Python 3.9, where truststore is unavailable, `system_context()` on macOS
34
+ trusts the Keychain's anchors (system roots, and those an administrator or
35
+ the user trusted) in addition to OpenSSL's default store.
36
+ - `create_context()` and `system_context()` take `ca=`, `certificate=`,
37
+ `key=`, `password=` and `identity=`. Every certificate, key, CA and
38
+ bundle argument (including `load_pem()` and `load_pkcs12()`) accepts PEM
39
+ text, PEM or DER bytes, a path, an object with `open()` or `read()`, a
40
+ `cryptography` certificate or private key, or a list of them.
41
+ - Apple's `/usr/bin/python3` (LibreSSL 2.8) is supported: keystore
42
+ identities sign through it over TLS 1.2, and client certificates are
43
+ chosen when the server asks.
44
+ - `system_context()`: a client context that behaves like the system's
45
+ browser: OS verification through truststore (a dependency on Python 3.10+) and
46
+ client certificates chosen when the server asks by a
47
+ `ClientCertificateSelector` over `system_sources()` (the Windows
48
+ Personal store, the default PKCS#11 module elsewhere), filtered as a
49
+ browser filters (validity, client-authentication usage, key type, the
50
+ CAs the server names) and picked by remembered choice, `AutoSelectRule`
51
+ rules, the Edge/Chrome/Chromium `AutoSelectCertificateForUrls` policy
52
+ (`browser_policies()`), a `choose` callback or the only candidate.
53
+ - `create_context()` builds a verifying `ssl.SSLContext` from
54
+ `verify`/`cafile`/`capath`/`cadata`, TLS version bounds and ALPN.
55
+ - `load_pem()` and `load_pkcs12()` load a certificate, chain and key from
56
+ memory, with no temporary files.
57
+ - `Identity`, the `Signer` protocol and `load_identity()`: a certificate whose
58
+ private key is used only through a signer, for every handshake signature
59
+ (RSA PKCS#1 v1.5 and PSS, ECDSA; TLS 1.2 and 1.3). OpenSSL 1.1.1 and 3.x.
60
+ - `sslcontext.testing.SoftwareSigner` and `software_identity()`.
61
+ - `sslcontext.issuing`: `CertificateAuthority` (create, or load from PEM;
62
+ optional X.509 name constraints; encrypted key export) issuing server
63
+ certificates (`IssuedCertificate`) that pass strict verification, made with
64
+ the interpreter's own OpenSSL.
65
+ - `Identity.info` (`CertificateInfo`: subject, issuer, serial, validity,
66
+ SHA-1/SHA-256 thumbprints, key type and size).
67
+ - `sslcontext.backends`: the `Backend` contract with thumbprint, subject,
68
+ issuer, label and validity queries, a registry (`names()`, `get()`,
69
+ `register_backend()`, entry-point group `sslcontext.backends`), and
70
+ `CNGBackend` for Windows certificate stores: non-exportable, TPM and
71
+ smart-card keys through CNG, and legacy CryptoAPI CSP keys (PKCS#1 v1.5,
72
+ TLS 1.2).
73
+ - PIN and password sources: `pin=` on every backend takes a PIN,
74
+ `KEYSTORE_PROMPT`, or a callback receiving a `PinRequest` (backend, token,
75
+ certificate, purpose); with no PIN, an operation needing one fails at once
76
+ instead of showing a dialog or waiting on a PIN pad. `load_pem()`,
77
+ `load_pkcs12()` and `SoftwareSigner` accept a password callable, called
78
+ only for an encrypted key.
79
+ - `PKCS11Backend` for PKCS#11 tokens: smart cards (real and virtual, through
80
+ OpenSC), HSMs, TPMs and software tokens. RSA PKCS#1 v1.5 and PSS and
81
+ ECDSA, PINs from the PIN source (see below),
82
+ per-signature login for `CKA_ALWAYS_AUTHENTICATE` keys, and recovery after
83
+ the card is removed and reinserted. `SSLCONTEXT_PKCS11_MODULE` selects the
84
+ default module.
85
+
86
+ ### Changed
87
+
88
+ - Rewritten from scratch. `sslcontext` now loads certificates and keystore-held
89
+ private keys into the standard library's `ssl.SSLContext`; nothing of the
90
+ 1.x API (`SSLContext`/`SSLSocket` interfaces, `PyOpenSSLContext`,
91
+ `SSLContextProvider`) remains. The 1.x companions `sslcontext-factory`,
92
+ `openssl-engine` and `openssl-engine-capi` are superseded: OpenSSL 4 removed
93
+ the ENGINE API they were built on.
94
+
95
+ [Unreleased]: https://github.com/jose-pr/sslcontext/compare/v2.1.0...HEAD
96
+ [2.1.0]: https://github.com/jose-pr/sslcontext/compare/v2.0.0...v2.1.0
97
+ [2.0.0]: https://github.com/jose-pr/sslcontext/releases/tag/v2.0.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jose A.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,168 @@
1
+ Metadata-Version: 2.5
2
+ Name: sslcontext
3
+ Version: 2.1.0
4
+ Summary: Client certificates from OS keystores (Windows CNG, macOS Keychain, PKCS#11 smart cards) in a standard ssl.SSLContext, chosen as the browser would
5
+ Project-URL: Homepage, https://github.com/jose-pr/sslcontext/
6
+ Project-URL: Documentation, https://jose-pr.github.io/sslcontext/
7
+ Project-URL: Issues, https://github.com/jose-pr/sslcontext/issues
8
+ Author: Jose A.
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Operating System :: MacOS
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Security :: Cryptography
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: truststore<0.11,>=0.10; python_version >= '3.10'
20
+ Provides-Extra: cli
21
+ Requires-Dist: duho<0.8,>=0.7.4; extra == 'cli'
22
+ Provides-Extra: dev
23
+ Requires-Dist: black; extra == 'dev'
24
+ Requires-Dist: build; extra == 'dev'
25
+ Requires-Dist: duho<0.8,>=0.7.4; extra == 'dev'
26
+ Requires-Dist: pytest; extra == 'dev'
27
+ Provides-Extra: docs
28
+ Requires-Dist: mkdocs; extra == 'docs'
29
+ Requires-Dist: mkdocs-material; extra == 'docs'
30
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # sslcontext
34
+
35
+ [![Version](https://img.shields.io/pypi/v/sslcontext.svg)](https://pypi.org/project/sslcontext/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/sslcontext.svg)](https://pypi.org/project/sslcontext/)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/sslcontext/blob/main/LICENSE)
38
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/sslcontext/)
39
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/sslcontext/test.yml)](https://github.com/jose-pr/sslcontext/actions/workflows/test.yml)
40
+
41
+ **Client certificates whose private keys never leave the OS keystore, in a
42
+ plain `ssl.SSLContext`.** `sslcontext` signs TLS handshakes through Windows
43
+ CNG, the macOS Keychain or a PKCS#11 token (smart cards included), so every
44
+ library that accepts an `ssl_context` gets mutual TLS with no patching, no
45
+ temporary key files and no compiled extension; `system_context()` also picks the certificate as the
46
+ browser would. Documentation: <https://jose-pr.github.io/sslcontext/>.
47
+
48
+ ## Features
49
+
50
+ - **Keystore identities in stdlib `ssl`** — non-exportable and smart-card
51
+ keys work, because the key is used through the keystore, never copied.
52
+ - **Pure Python** — ctypes over the OpenSSL that Python itself loaded: an
53
+ OpenSSL 3 provider on modern builds, an OpenSSL 1.1 engine on older ones.
54
+ - **In-memory credentials** — load PEM or PKCS#12 bytes into a context
55
+ without writing a key to disk.
56
+ - **Never blocks on a PIN** — PINs and passwords come from a value or a
57
+ callback you write; with none, operations fail at once. Keystore dialogs
58
+ and PIN pads are opt-in.
59
+ - **The system's context** — `system_context()`: the OS verifies servers
60
+ (truststore) and, when a server asks, the client certificate is chosen
61
+ as the browser would: from the system's keystores, matching the CAs the
62
+ server names, following Edge's and Chrome's `AutoSelectCertificateForUrls`
63
+ policy, your rules or your callback, and never blocking.
64
+ - **One context builder** — `create_context()` for the verify/cafile/capath
65
+ combinations every HTTP client reimplements.
66
+
67
+ ### Keystores
68
+
69
+ | Backend | Platform | Keys | Extra |
70
+ | --- | --- | --- | --- |
71
+ | `cng` | Windows | certificate stores: software (incl. non-exportable), TPM, smart cards and virtual smart cards, legacy CryptoAPI CSPs | none |
72
+ | `pkcs11` | any | PKCS#11 tokens: smart cards (real or virtual) via OpenSC, HSMs, TPMs, SoftHSM | none |
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pip install sslcontext
78
+ ```
79
+
80
+ ## Quick start
81
+
82
+ Behave like the system's browser: the OS verifies the server, and when a
83
+ server asks for a client certificate the matching one is chosen from the
84
+ system's keystores. Build it once and share it:
85
+
86
+ ```python
87
+ import urllib.request
88
+ import sslcontext
89
+
90
+ ctx = sslcontext.system_context()
91
+ urllib.request.urlopen("https://intranet.example.com/", context=ctx)
92
+ ```
93
+
94
+ A client certificate and key, from files, PEM text, bytes, `pathlib`
95
+ paths or `cryptography` objects alike:
96
+
97
+ ```python
98
+ import sslcontext
99
+
100
+ ctx = sslcontext.create_context(ca="ca.pem", certificate="client.pem", key=key_pem)
101
+ ```
102
+
103
+ Use a certificate from the Windows store whose key cannot be exported:
104
+
105
+ ```python
106
+ import urllib.request
107
+ import sslcontext
108
+ from sslcontext.backends import CNGBackend
109
+
110
+ ctx = sslcontext.create_context()
111
+ sslcontext.load_identity(ctx, CNGBackend().find_identity(subject="alice"))
112
+ urllib.request.urlopen("https://mtls.example.com/", context=ctx)
113
+ ```
114
+
115
+ Use a smart card through its PKCS#11 module. The PIN callback decides how
116
+ the PIN is obtained; with no PIN, signing fails instead of waiting:
117
+
118
+ ```python
119
+ from getpass import getpass
120
+ from sslcontext.backends import PKCS11Backend
121
+
122
+ def pin(request: sslcontext.PinRequest):
123
+ if request.purpose == "list":
124
+ return None # list without logging in
125
+ return getpass(f"PIN for {request.token}: ")
126
+
127
+ card = PKCS11Backend(pin=pin)
128
+ sslcontext.load_identity(ctx, card.find_identity(subject="alice"))
129
+ ```
130
+
131
+ ## API overview
132
+
133
+ | Module | Purpose |
134
+ | --- | --- |
135
+ | `sslcontext` | context builders (`system_context`, `create_context`), identity loading, client certificate selection, the `Signer` contract |
136
+ | `sslcontext.backends` | keystore backends and their registry |
137
+ | `sslcontext.issuing` | a local certificate authority issuing server certificates |
138
+ | `sslcontext.testing` | `SoftwareSigner`, an in-memory signer for tests |
139
+ | `sslcontext.exceptions` | the package's errors |
140
+
141
+ ## Command line
142
+
143
+ `pip install "sslcontext[cli]"` adds `sslcontext`: `sslcontext list` shows the
144
+ client certificates the system's keystores hold, and `sslcontext probe
145
+ https://intranet.example/` shows what a server asks for and which
146
+ certificate answers it.
147
+
148
+ ## Development
149
+
150
+ ```bash
151
+ dotagents pyvenv && dotagents pyvenv 3.9
152
+ .pyvenv/<tag>/Scripts/python -m pip install -e ".[dev]"
153
+ .pyvenv/<tag>/Scripts/python -m pytest -q -rs
154
+ ```
155
+
156
+ ### Releasing
157
+
158
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
159
+ [`CHANGELOG.md`](https://github.com/jose-pr/sslcontext/blob/main/CHANGELOG.md).
160
+ Pushing a tag matching `v*` triggers the release workflow: test gate → build
161
+ (checking the tag names the version built) → a strict docs build as a gate →
162
+ GitHub release → publish. The release workflow never deploys the docs site
163
+ itself: for a final release its last job dispatches the docs workflow at the
164
+ tag, which owns every Pages deploy.
165
+
166
+ ## License
167
+
168
+ MIT — see [LICENSE](https://github.com/jose-pr/sslcontext/blob/main/LICENSE).
@@ -0,0 +1,136 @@
1
+ # sslcontext
2
+
3
+ [![Version](https://img.shields.io/pypi/v/sslcontext.svg)](https://pypi.org/project/sslcontext/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/sslcontext.svg)](https://pypi.org/project/sslcontext/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/sslcontext/blob/main/LICENSE)
6
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/sslcontext/)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/sslcontext/test.yml)](https://github.com/jose-pr/sslcontext/actions/workflows/test.yml)
8
+
9
+ **Client certificates whose private keys never leave the OS keystore, in a
10
+ plain `ssl.SSLContext`.** `sslcontext` signs TLS handshakes through Windows
11
+ CNG, the macOS Keychain or a PKCS#11 token (smart cards included), so every
12
+ library that accepts an `ssl_context` gets mutual TLS with no patching, no
13
+ temporary key files and no compiled extension; `system_context()` also picks the certificate as the
14
+ browser would. Documentation: <https://jose-pr.github.io/sslcontext/>.
15
+
16
+ ## Features
17
+
18
+ - **Keystore identities in stdlib `ssl`** — non-exportable and smart-card
19
+ keys work, because the key is used through the keystore, never copied.
20
+ - **Pure Python** — ctypes over the OpenSSL that Python itself loaded: an
21
+ OpenSSL 3 provider on modern builds, an OpenSSL 1.1 engine on older ones.
22
+ - **In-memory credentials** — load PEM or PKCS#12 bytes into a context
23
+ without writing a key to disk.
24
+ - **Never blocks on a PIN** — PINs and passwords come from a value or a
25
+ callback you write; with none, operations fail at once. Keystore dialogs
26
+ and PIN pads are opt-in.
27
+ - **The system's context** — `system_context()`: the OS verifies servers
28
+ (truststore) and, when a server asks, the client certificate is chosen
29
+ as the browser would: from the system's keystores, matching the CAs the
30
+ server names, following Edge's and Chrome's `AutoSelectCertificateForUrls`
31
+ policy, your rules or your callback, and never blocking.
32
+ - **One context builder** — `create_context()` for the verify/cafile/capath
33
+ combinations every HTTP client reimplements.
34
+
35
+ ### Keystores
36
+
37
+ | Backend | Platform | Keys | Extra |
38
+ | --- | --- | --- | --- |
39
+ | `cng` | Windows | certificate stores: software (incl. non-exportable), TPM, smart cards and virtual smart cards, legacy CryptoAPI CSPs | none |
40
+ | `pkcs11` | any | PKCS#11 tokens: smart cards (real or virtual) via OpenSC, HSMs, TPMs, SoftHSM | none |
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install sslcontext
46
+ ```
47
+
48
+ ## Quick start
49
+
50
+ Behave like the system's browser: the OS verifies the server, and when a
51
+ server asks for a client certificate the matching one is chosen from the
52
+ system's keystores. Build it once and share it:
53
+
54
+ ```python
55
+ import urllib.request
56
+ import sslcontext
57
+
58
+ ctx = sslcontext.system_context()
59
+ urllib.request.urlopen("https://intranet.example.com/", context=ctx)
60
+ ```
61
+
62
+ A client certificate and key, from files, PEM text, bytes, `pathlib`
63
+ paths or `cryptography` objects alike:
64
+
65
+ ```python
66
+ import sslcontext
67
+
68
+ ctx = sslcontext.create_context(ca="ca.pem", certificate="client.pem", key=key_pem)
69
+ ```
70
+
71
+ Use a certificate from the Windows store whose key cannot be exported:
72
+
73
+ ```python
74
+ import urllib.request
75
+ import sslcontext
76
+ from sslcontext.backends import CNGBackend
77
+
78
+ ctx = sslcontext.create_context()
79
+ sslcontext.load_identity(ctx, CNGBackend().find_identity(subject="alice"))
80
+ urllib.request.urlopen("https://mtls.example.com/", context=ctx)
81
+ ```
82
+
83
+ Use a smart card through its PKCS#11 module. The PIN callback decides how
84
+ the PIN is obtained; with no PIN, signing fails instead of waiting:
85
+
86
+ ```python
87
+ from getpass import getpass
88
+ from sslcontext.backends import PKCS11Backend
89
+
90
+ def pin(request: sslcontext.PinRequest):
91
+ if request.purpose == "list":
92
+ return None # list without logging in
93
+ return getpass(f"PIN for {request.token}: ")
94
+
95
+ card = PKCS11Backend(pin=pin)
96
+ sslcontext.load_identity(ctx, card.find_identity(subject="alice"))
97
+ ```
98
+
99
+ ## API overview
100
+
101
+ | Module | Purpose |
102
+ | --- | --- |
103
+ | `sslcontext` | context builders (`system_context`, `create_context`), identity loading, client certificate selection, the `Signer` contract |
104
+ | `sslcontext.backends` | keystore backends and their registry |
105
+ | `sslcontext.issuing` | a local certificate authority issuing server certificates |
106
+ | `sslcontext.testing` | `SoftwareSigner`, an in-memory signer for tests |
107
+ | `sslcontext.exceptions` | the package's errors |
108
+
109
+ ## Command line
110
+
111
+ `pip install "sslcontext[cli]"` adds `sslcontext`: `sslcontext list` shows the
112
+ client certificates the system's keystores hold, and `sslcontext probe
113
+ https://intranet.example/` shows what a server asks for and which
114
+ certificate answers it.
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ dotagents pyvenv && dotagents pyvenv 3.9
120
+ .pyvenv/<tag>/Scripts/python -m pip install -e ".[dev]"
121
+ .pyvenv/<tag>/Scripts/python -m pytest -q -rs
122
+ ```
123
+
124
+ ### Releasing
125
+
126
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
127
+ [`CHANGELOG.md`](https://github.com/jose-pr/sslcontext/blob/main/CHANGELOG.md).
128
+ Pushing a tag matching `v*` triggers the release workflow: test gate → build
129
+ (checking the tag names the version built) → a strict docs build as a gate →
130
+ GitHub release → publish. The release workflow never deploys the docs site
131
+ itself: for a final release its last job dispatches the docs workflow at the
132
+ tag, which owns every Pages deploy.
133
+
134
+ ## License
135
+
136
+ MIT — see [LICENSE](https://github.com/jose-pr/sslcontext/blob/main/LICENSE).
@@ -0,0 +1,41 @@
1
+ # API Reference
2
+
3
+ Every public name, generated from the package's own docstrings.
4
+
5
+ The same surface in prose, with the bounds and the gotchas, is the API header
6
+ that ships inside the installed package, `sslcontext/AGENTS.md`, readable with
7
+ `importlib.resources.files("sslcontext")` and printed in the Guide of this site.
8
+
9
+ ::: sslcontext
10
+ options:
11
+ show_root_heading: false
12
+ members_order: source
13
+ filters:
14
+ - "!^_"
15
+
16
+ ## Keystore backends
17
+
18
+ ::: sslcontext.backends
19
+ options:
20
+ show_root_heading: false
21
+ members_order: source
22
+ filters:
23
+ - "!^_"
24
+
25
+ ## Issuing certificates
26
+
27
+ ::: sslcontext.issuing
28
+ options:
29
+ show_root_heading: false
30
+ members_order: source
31
+ filters:
32
+ - "!^_"
33
+
34
+ ## Test helpers
35
+
36
+ ::: sslcontext.testing
37
+ options:
38
+ show_root_heading: false
39
+ members_order: source
40
+ filters:
41
+ - "!^_"
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ --8<-- "CHANGELOG.md"
@@ -0,0 +1,7 @@
1
+ # The API header
2
+
3
+ The reference that ships inside the installed package as
4
+ `sslcontext/AGENTS.md`: every public name with its signature, contract and
5
+ gotchas.
6
+
7
+ --8<-- "src/sslcontext/AGENTS.md"
@@ -0,0 +1,59 @@
1
+ # sslcontext
2
+
3
+ **Client certificates whose private keys never leave the OS keystore, in a
4
+ plain `ssl.SSLContext`.** `sslcontext` signs TLS handshakes through Windows
5
+ CNG, the macOS Keychain or a PKCS#11 token (smart cards included), so every
6
+ library that takes an `ssl_context` gets mutual TLS with no patching, no
7
+ temporary key files and no compiled extension. It drives the TLS library your
8
+ Python already loaded, through ctypes: OpenSSL 1.1.1 and 3.x, and the
9
+ LibreSSL of Apple's bundled Python. Nothing ever waits for a PIN unless you
10
+ say how one is given.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install sslcontext
16
+ ```
17
+
18
+ Requires Python 3.9 or newer.
19
+
20
+ | Extra | Adds | Needed for |
21
+ | --- | --- | --- |
22
+ | `cli` | `duho` | the `sslcontext` command |
23
+
24
+ ## 30-second tour
25
+
26
+ Behave like the system's browser: the OS verifies the server and, when a
27
+ server asks for a client certificate, the matching one is chosen from the
28
+ system's keystores. Build it once and share it:
29
+
30
+ ```python
31
+ import urllib.request
32
+ import sslcontext
33
+
34
+ ctx = sslcontext.system_context()
35
+ urllib.request.urlopen("https://intranet.example.com/", context=ctx)
36
+ ```
37
+
38
+ Or name the certificate yourself:
39
+
40
+ ```python
41
+ from sslcontext.backends import CNGBackend, PKCS11Backend
42
+
43
+ ctx = sslcontext.create_context()
44
+ sslcontext.load_identity(ctx, CNGBackend().find_identity(subject="alice"))
45
+ # or a smart card: PKCS11Backend(pin=...).find_identity(label="...")
46
+ ```
47
+
48
+ See which certificate a server would get:
49
+
50
+ ```bash
51
+ sslcontext list
52
+ sslcontext probe https://intranet.example.com/
53
+ ```
54
+
55
+ ## Learn more
56
+
57
+ - [The API header](header.md): every public name, its contract and gotchas.
58
+ - [API reference](api/reference.md), generated from the docstrings.
59
+ - [Changelog](changelog.md)
@@ -0,0 +1,52 @@
1
+ site_name: sslcontext
2
+ site_description: "Client certificates from OS keystores (Windows CNG, PKCS#11 smart cards) in a standard ssl.SSLContext, chosen as the browser would"
3
+ site_url: https://jose-pr.github.io/sslcontext/
4
+ repo_url: https://github.com/jose-pr/sslcontext
5
+ repo_name: jose-pr/sslcontext
6
+ edit_uri: edit/main/docs/
7
+
8
+ # Constrain the scan to docs/ so pymdownx.snippets and strict nav do not trip
9
+ # on repo-level markdown.
10
+ docs_dir: docs
11
+
12
+ theme:
13
+ name: material
14
+ palette:
15
+ - media: "(prefers-color-scheme: light)"
16
+ scheme: default
17
+ toggle:
18
+ icon: material/brightness-7
19
+ name: Switch to dark mode
20
+ - media: "(prefers-color-scheme: dark)"
21
+ scheme: slate
22
+ toggle:
23
+ icon: material/brightness-4
24
+ name: Switch to light mode
25
+ features:
26
+ - navigation.sections
27
+ - content.code.copy
28
+
29
+ nav:
30
+ - Home: index.md
31
+ - Guide:
32
+ - The API header: header.md
33
+ - API Reference:
34
+ - Overview: api/reference.md
35
+ - Changelog: changelog.md
36
+
37
+ plugins:
38
+ - search
39
+ - mkdocstrings:
40
+ handlers:
41
+ python:
42
+ options:
43
+ show_source: true
44
+ show_root_heading: true
45
+ merge_init_into_class: true
46
+
47
+ markdown_extensions:
48
+ - admonition
49
+ - tables
50
+ - pymdownx.snippets:
51
+ base_path: ["."]
52
+ - pymdownx.superfences