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.
- sslcontext-2.1.0/.gitignore +37 -0
- sslcontext-2.1.0/CHANGELOG.md +97 -0
- sslcontext-2.1.0/LICENSE +21 -0
- sslcontext-2.1.0/PKG-INFO +168 -0
- sslcontext-2.1.0/README.md +136 -0
- sslcontext-2.1.0/docs/api/reference.md +41 -0
- sslcontext-2.1.0/docs/changelog.md +3 -0
- sslcontext-2.1.0/docs/header.md +7 -0
- sslcontext-2.1.0/docs/index.md +59 -0
- sslcontext-2.1.0/mkdocs.yml +52 -0
- sslcontext-2.1.0/pyproject.toml +59 -0
- sslcontext-2.1.0/src/sslcontext/AGENTS.md +644 -0
- sslcontext-2.1.0/src/sslcontext/__init__.py +80 -0
- sslcontext-2.1.0/src/sslcontext/__main__.py +5 -0
- sslcontext-2.1.0/src/sslcontext/_asn1.py +118 -0
- sslcontext-2.1.0/src/sslcontext/_context.py +106 -0
- sslcontext-2.1.0/src/sslcontext/_identity.py +112 -0
- sslcontext-2.1.0/src/sslcontext/_load.py +241 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/__init__.py +8 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_api.py +261 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_certcb.py +219 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_ctx.py +58 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_engine.py +268 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_errors.py +22 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_keys.py +77 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_loader.py +152 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_objects.py +225 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_params.py +134 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_provider.py +692 -0
- sslcontext-2.1.0/src/sslcontext/_openssl/_signing.py +43 -0
- sslcontext-2.1.0/src/sslcontext/_pin.py +126 -0
- sslcontext-2.1.0/src/sslcontext/_policy.py +158 -0
- sslcontext-2.1.0/src/sslcontext/_select.py +448 -0
- sslcontext-2.1.0/src/sslcontext/_sources.py +99 -0
- sslcontext-2.1.0/src/sslcontext/_system.py +188 -0
- sslcontext-2.1.0/src/sslcontext/backends/__init__.py +189 -0
- sslcontext-2.1.0/src/sslcontext/backends/_cng.py +549 -0
- sslcontext-2.1.0/src/sslcontext/backends/_cryptoki.py +348 -0
- sslcontext-2.1.0/src/sslcontext/backends/_keychain.py +341 -0
- sslcontext-2.1.0/src/sslcontext/backends/_macos.py +257 -0
- sslcontext-2.1.0/src/sslcontext/backends/_pkcs11.py +406 -0
- sslcontext-2.1.0/src/sslcontext/cli/__init__.py +36 -0
- sslcontext-2.1.0/src/sslcontext/cli/_common.py +113 -0
- sslcontext-2.1.0/src/sslcontext/cli/_keystore.py +99 -0
- sslcontext-2.1.0/src/sslcontext/cli/_probe.py +166 -0
- sslcontext-2.1.0/src/sslcontext/cli/_root.py +30 -0
- sslcontext-2.1.0/src/sslcontext/exceptions.py +45 -0
- sslcontext-2.1.0/src/sslcontext/issuing.py +421 -0
- sslcontext-2.1.0/src/sslcontext/testing.py +154 -0
- sslcontext-2.1.0/tests/conftest.py +148 -0
- sslcontext-2.1.0/tests/data/ca.pem +11 -0
- sslcontext-2.1.0/tests/data/client-ec.der +0 -0
- sslcontext-2.1.0/tests/data/client-ec.enc.der +0 -0
- sslcontext-2.1.0/tests/data/client-ec.enc.key +8 -0
- sslcontext-2.1.0/tests/data/client-ec.key +5 -0
- sslcontext-2.1.0/tests/data/client-ec.pem +11 -0
- sslcontext-2.1.0/tests/data/client-rsa.key +28 -0
- sslcontext-2.1.0/tests/data/client-rsa.p12 +0 -0
- sslcontext-2.1.0/tests/data/client-rsa.pem +16 -0
- sslcontext-2.1.0/tests/data/server.key +28 -0
- sslcontext-2.1.0/tests/data/server.pem +16 -0
- sslcontext-2.1.0/tests/integration/start_virtual_card.sh +27 -0
- sslcontext-2.1.0/tests/integration/test_cng.py +189 -0
- sslcontext-2.1.0/tests/integration/test_keychain.py +126 -0
- sslcontext-2.1.0/tests/integration/test_pkcs11.py +350 -0
- sslcontext-2.1.0/tests/test_backends.py +71 -0
- sslcontext-2.1.0/tests/test_cli.py +120 -0
- sslcontext-2.1.0/tests/test_context.py +110 -0
- sslcontext-2.1.0/tests/test_issuing.py +102 -0
- sslcontext-2.1.0/tests/test_load.py +86 -0
- sslcontext-2.1.0/tests/test_package.py +5 -0
- sslcontext-2.1.0/tests/test_pin.py +90 -0
- sslcontext-2.1.0/tests/test_select.py +415 -0
- sslcontext-2.1.0/tests/test_signer.py +163 -0
- sslcontext-2.1.0/tests/test_sources.py +246 -0
- sslcontext-1.4.3/PKG-INFO +0 -15
- sslcontext-1.4.3/setup.cfg +0 -4
- sslcontext-1.4.3/setup.py +0 -89
- sslcontext-1.4.3/src/sslcontext/__init__.py +0 -13
- sslcontext-1.4.3/src/sslcontext/_vendor/_utils.py +0 -8
- sslcontext-1.4.3/src/sslcontext/_vendor/imports.py +0 -8
- sslcontext-1.4.3/src/sslcontext/_vendor/pyopenssl.py +0 -418
- sslcontext-1.4.3/src/sslcontext/_vendor/ssl.py +0 -81
- sslcontext-1.4.3/src/sslcontext/_vendor/wait.py +0 -115
- sslcontext-1.4.3/src/sslcontext/interface.py +0 -169
- sslcontext-1.4.3/src/sslcontext/utils.py +0 -50
- sslcontext-1.4.3/src/sslcontext.egg-info/PKG-INFO +0 -15
- sslcontext-1.4.3/src/sslcontext.egg-info/SOURCES.txt +0 -15
- sslcontext-1.4.3/src/sslcontext.egg-info/dependency_links.txt +0 -1
- sslcontext-1.4.3/src/sslcontext.egg-info/requires.txt +0 -9
- sslcontext-1.4.3/src/sslcontext.egg-info/top_level.txt +0 -1
- /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
|
sslcontext-2.1.0/LICENSE
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/sslcontext/)
|
|
36
|
+
[](https://pypi.org/project/sslcontext/)
|
|
37
|
+
[](https://github.com/jose-pr/sslcontext/blob/main/LICENSE)
|
|
38
|
+
[](https://jose-pr.github.io/sslcontext/)
|
|
39
|
+
[](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
|
+
[](https://pypi.org/project/sslcontext/)
|
|
4
|
+
[](https://pypi.org/project/sslcontext/)
|
|
5
|
+
[](https://github.com/jose-pr/sslcontext/blob/main/LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/sslcontext/)
|
|
7
|
+
[](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,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
|