The API header
The reference that ships inside the installed package as
sslcontext/AGENTS.md: every public name with its signature, contract and
gotchas.
sslcontext — public API header
Header-file-style reference for the sslcontext package: every public export with
its signature, arguments, contract and gotchas, so the package can be used
without reading its source. It ships inside the package and is
self-contained. Development documentation lives with the source at
https://github.com/jose-pr/sslcontext.
Import everything from the root sslcontext, except the errors, which live in
sslcontext.exceptions and are re-exported from the root, the keystore
backends (sslcontext.backends) and the test helpers (sslcontext.testing).
Modules whose names start with _ are private.
pip install sslcontext. One dependency, truststore (Python 3.10+ only),
for system_context()'s OS verification; the extra cli adds the
sslcontext command (duho).
sslcontext.__version__ is the installed distribution's version.
Building a context (sslcontext)
def create_context(
purpose: str = "client",
*,
verify: bool | str | os.PathLike | None = None,
cafile: str | os.PathLike | None = None,
capath: str | os.PathLike | None = None,
cadata: str | bytes | None = None,
ca: CertificatesLike | None = None,
certificate: CertificatesLike | None = None,
key: KeyLike | None = None,
password: PasswordSource = None,
identity: Identity | None = None,
minimum_version: ssl.TLSVersion | None = None,
maximum_version: ssl.TLSVersion | None = None,
alpn: Iterable[str] | None = None,
) -> ssl.SSLContext
Returns a plain ssl.SSLContext built on ssl.create_default_context.
purpose is "client" or "server" (anything else: ValueError).
verify=None keeps the purpose's default: a client verifies the server and
its host name; a server does not ask for a client certificate. True turns
verification on (a server then requires a client certificate); False turns
it off (a client stops checking the host name too). A path in verify means
"trust this CA bundle file, or this hashed CA directory", as in requests;
combining it with cafile/capath is a ValueError. Trust comes from
ca/cafile/capath/cadata when given, else the platform's default
store; ca with cadata is a ValueError. certificate, key and
password load the context's own certificate as load_pem does (a key
alone is a ValueError); identity as load_identity does. A client
context with a certificate is an mTLS context in one call:
ctx = sslcontext.create_context(ca="ca.pem", certificate="client.pem", key="client.key")
VerifyLike is the exported alias for the verify type.
Certificate and key sources
CertificateLike = str | bytes | os.PathLike | <has open()> | <has read()> | cryptography.x509.Certificate
CertificatesLike = CertificateLike | list[CertificateLike] | tuple[CertificateLike, ...]
KeyLike = str | bytes | os.PathLike | <has open()> | <has read()> | <cryptography private key>
Every certificate, key, CA and bundle argument takes any of these. A str
holding a PEM header (-----BEGIN) is PEM text; any other str is a path.
Bytes are PEM or DER. os.PathLike is a path; an object with open()
(pathlib.Path, pathlib-next paths, zipfile.Path,
importlib.resources files) is opened "rb"; an object with read() is
read (text is encoded as ASCII). A cryptography certificate (it has
public_bytes and fingerprint) or private key (it has private_bytes;
written unencrypted, in memory only) is converted; cryptography is never
imported otherwise. A list holds several sources in order: a CA bundle, or
a leaf followed by its chain; an empty list is a ValueError. Anything else
is a TypeError; a file that cannot be read raises its OSError (a
mistyped path in a str is FileNotFoundError, never "not PEM").
The system's context: OS trust and browser-like client certificates (sslcontext)
def system_context(*, selector: ClientCertificateSelector | bool | None = None,
pin: PinSource = None, verify=None, ca=None, cafile=None,
capath=None, cadata=None, certificate=None, key=None,
password=None, identity=None, minimum_version=None,
maximum_version=None, alpn=None) -> ssl.SSLContext
def system_selector(*, pin: PinSource = None, choose: Chooser | None = None,
rules: Iterable[AutoSelectRule] = (), browser_policy: bool = True,
remember: bool = True, cache_ttl: float = 60.0
) -> ClientCertificateSelector
def system_sources(*, pin: PinSource = None) -> list[Backend]
system_context() is a client context that behaves like the system's
browser. Servers are verified by the operating system (a
truststore.SSLContext: CryptoAPI on Windows, the Security framework on
macOS) on Python 3.10+, and by OpenSSL over the default store on 3.9
(on macOS with the Keychain's trust anchors added: the system roots and
those an administrator or the user trusted, none marked "deny")
(truststore needs 3.10); verify/ca/cafile/capath/cadata replace
that trust exactly as for create_context. When a server asks for a client
certificate, selector answers (default system_selector(pin=pin);
False: never send one). A certificate (+ key, password) or
identity is loaded as by create_context and is then the certificate
sent: the default selector is not installed then (pass selector=True or
one of your own to have both).
post_handshake_auth is on. Nothing is opened until a server asks; one
context serves every server and every thread, so share it (urllib3 keys its
pools by context). system_sources(): Windows CNGBackend("MY") (smart-card
certificates appear there while the card is inserted); macOS
KeychainBackend() (the user's keychains and smart cards); elsewhere
PKCS11Backend() when its default module exists (p11-kit's proxy on Linux),
else [].
class ClientCertificateSelector:
def __init__(self, sources: Iterable[IdentitySource], *, choose: Chooser | None = None,
rules: Iterable[AutoSelectRule] = (), browser_policy: bool = False,
remember: bool = True, cache_ttl: float = 60.0)
def install(self, context: ssl.SSLContext) -> None # client contexts; replaces a previous one
def candidates(self, request: CertificateRequest) -> list[Identity]
def select(self, request: CertificateRequest) -> Identity | None
def identities(self) -> list[Identity] # every source, cached cache_ttl s
def policies(self) -> tuple[AutoSelectRule, ...] # browser rules, cached; () unless browser_policy
def refresh(self) -> None # list sources and policies again
def forget(self, host: str | None = None) -> None # drop remembered choices
IdentitySource = Backend | Identity | Callable[[], Iterable[Identity]]
Chooser = Callable[[CertificateRequest, list[Identity]], Identity | None]
@dataclass(frozen=True)
class CertificateRequest:
host: str | None # the SNI the client sent
authorities: tuple[str, ...] # RFC 4514 names of acceptable CAs; () = any
authorities_der: tuple[bytes, ...]
key_types: frozenset[str] # "rsa" / "ec" the server's signature algorithms allow
rsa_pss_required: bool # TLS 1.3, or no PKCS#1 offered
tls_version: str # "TLSv1.3", "TLSv1.2", ...
The selector answers each certificate request during the handshake, on that
connection only. Candidates are what a browser offers: valid now; no
extended key usage, clientAuth or anyExtendedKeyUsage, and
digitalSignature when key usage is present; a key type in key_types (an
RSA key whose signer has supports_pss == False is left out when PSS is
required); issued under a named authority (the leaf's issuer, or any subject
or issuer in its chain) when the server names any. Newest expiry first. The
choice, in order: the identity remembered for the host (when still a
candidate), the first rules entry matching the host with a matching
candidate, the same over browser_policies() when browser_policy, then
choose(request, candidates); with no choose, the only candidate when
there is exactly one. Otherwise no certificate is sent (logged at INFO) and
an identity loaded with load_identity (if any) is sent instead, else none;
the server decides. A source that raises KeystoreError/OSError while
listing is logged at WARNING and skipped; an identity whose certificate or
key cannot be examined is skipped. An exception from choose, or a result
that is not one of the candidates, fails that handshake (ssl.SSLError,
logged at WARNING). install keeps the selector alive as long as the
context. Sources are listed at most every cache_ttl seconds.
@dataclass(frozen=True)
class AutoSelectRule:
pattern: str
issuer: str | Mapping[str, str] | None = None
subject: str | Mapping[str, str] | None = None
def matches_host(self, host: str | None) -> bool
def matches(self, identity: Identity) -> bool
def browser_policies() -> list[AutoSelectRule]
def rules_from_policy(entries: Iterable[str | Mapping]) -> list[AutoSelectRule]
AutoSelectRule is Chrome's and Edge's AutoSelectCertificateForUrls entry:
pattern example.com (that host), [*.]example.com (it and subdomains),
*.example.com (subdomains), * (any); a scheme, port or path in it is
ignored (the request knows only the host). issuer/subject: a mapping of
RFC 4514 attributes to exact values ({"CN": "Corp CA", "OU": "Staff"}), or
a string matched case-insensitively inside the whole name.
browser_policies() reads the policy set for Edge, Chrome and Chromium:
Windows registry (HKLM then HKCU, SOFTWARE\Policies\{Microsoft\Edge,
Google\Chrome,Chromium}\AutoSelectCertificateForUrls, values in numeric
order), Linux /etc/opt/{edge,chrome}/policies/managed/*.json and
/etc/chromium/policies/managed/*.json, macOS /Library/Managed
Preferences/[<user>/]{com.microsoft.Edge,com.google.Chrome,
org.chromium.Chromium}.plist. rules_from_policy parses entries
({"pattern": ..., "filter": {"ISSUER": {...}, "SUBJECT": {...}}}, as JSON
text or objects); a malformed entry is skipped, as the browsers skip it.
import sslcontext, urllib.request
ctx = sslcontext.system_context() # share it
urllib.request.urlopen("https://intranet.example", context=ctx)
selector = sslcontext.system_selector(
rules=[sslcontext.AutoSelectRule("[*.]corp.example", issuer={"CN": "Corp CA"})],
choose=lambda request, candidates: candidates[0], # instead of a dialog
)
ctx = sslcontext.system_context(selector=selector)
Loading credentials from memory (sslcontext)
def load_pem(
context: ssl.SSLContext,
certificate: CertificatesLike,
key: KeyLike | None = None,
password: PasswordSource = None,
) -> None
Makes certificate (+ chain) and key the context's identity; nothing is
written to disk. certificate is the leaf then its chain: PEM holding them
in order, one DER certificate, or a list of sources in that order; or one
PKCS#12 bundle (.p12/.pfx, recognised by its DER shape, whatever the file
is called), loaded as load_pkcs12 loads it, with password and without
key (a key, or a bundle in a list, is a ValueError). key is a PEM or
DER private key (DER: PKCS#8, encrypted or not, or the traditional RSA/EC
form) or a cryptography key; omitted, it is read from the first
certificate source (a combined PEM). password decrypts an encrypted key or
bundle; see PINs and passwords.
create_context(certificate=..., password=...) and system_context take the
same, so create_context(certificate="client.p12", password="...") is an
mTLS context.
def load_pkcs12(
context: ssl.SSLContext,
data: bytes | str | os.PathLike | <has open()> | <has read()>,
password: PasswordSource = None,
) -> None
Loads the key, certificate and chain of a PKCS#12 (.p12/.pfx) bundle,
given as its bytes or where it is (a str is a path here).
Both raise CredentialError for malformed data, a wrong or missing password,
or a key that does not match the certificate, and TypeError when context
is not an ssl.SSLContext. An identity of one key type (RSA, EC) replaces a
previously loaded identity of the same type; identities of different types
coexist and OpenSSL picks per handshake. BytesOrText (bytes or text) is
kept as an exported alias. Every loader (and install) configures
a truststore.SSLContext through the inner context its connections use.
Keystore identities (sslcontext)
class Signer(Protocol):
def sign(
self, digest: bytes, *, hash: str,
padding: Padding | None = None, salt_length: int | None = None,
) -> bytes
The private-key contract. digest is already hashed with hash, one of
HASHES ("sha1", "sha224", "sha256", "sha384", "sha512"). For an
RSA key padding is Padding.PKCS1V15 or Padding.PSS; with PSS,
salt_length is in bytes and MGF1 uses hash. For an EC key padding and
salt_length are None and the result must be a DER-encoded ECDSA
signature. Any exception fails the handshake; it is logged at WARNING on the
sslcontext logger. sign runs on the thread doing the handshake while
OpenSSL is mid-operation, so it must not use the same ssl.SSLContext.
TLS 1.3 always asks RSA keys for PSS.
class Padding(enum.Enum):
PKCS1V15 = "pkcs1v15"
PSS = "pss"
@dataclass(frozen=True)
class Identity:
certificate: bytes # DER
signer: Signer
chain: tuple[bytes, ...] = () # DER, issuer of certificate first
label: str = ""
Identity raises TypeError when certificate/chain are not DER bytes
or signer has no sign method. identity.info (cached) decodes the
certificate into a CertificateInfo:
@dataclass(frozen=True)
class CertificateInfo:
subject: str # RFC 4514, e.g. "CN=alice,O=Example"
issuer: str
serial: str # upper-case hex
not_before: datetime.datetime # UTC, timezone-aware
not_after: datetime.datetime
sha1: str # upper-case hex; Windows' "Thumbprint"
sha256: str
key_type: str # "rsa", "ec" or "other"
key_bits: int
def valid_at(self, when: datetime.datetime | None = None) -> bool
A signer may carry supports_pss: bool. When it is False on an RSA
identity, load_identity restricts the context's own signatures to PKCS#1
v1.5 RSA and ECDSA (SSL_CTX_set1_client_sigalgs_list), so that identity
can only complete TLS 1.2.
def load_identity(context: ssl.SSLContext, identity: Identity) -> None
Makes the identity the context's certificate and key; each handshake
signature is delegated to identity.signer. RSA and EC certificates only.
The signer is kept alive until context is garbage-collected. Raises
CredentialError (malformed certificate), UnsupportedOpenSSLError
(interpreter's OpenSSL cannot host the signer), TypeError. Works on
OpenSSL 1.1.1 and 3.x.
PINs and passwords (sslcontext)
Nothing waits on a user unless the caller chose it. Without a PIN, an
operation that needs one fails at once with KeystoreError; a keystore's own
prompt (Windows' PIN or consent dialog, a reader's PIN pad) is used only when
the caller passes or returns KEYSTORE_PROMPT.
class Prompt(enum.Enum):
KEYSTORE = "keystore"
KEYSTORE_PROMPT = Prompt.KEYSTORE
@dataclass(frozen=True)
class PinRequest:
backend: str # "cng" or "pkcs11"
token: str # PKCS#11 token label; CNG: the certificate's friendly name
certificate: str # subject of the certificate whose key needs it; "" when listing
purpose: str # "list", "sign" or "per-signature"
keystore_prompt: bool # whether KEYSTORE_PROMPT can work (PIN pad / Windows dialog)
PinCallback = Callable[[PinRequest], str | Prompt | None]
PinSource = str | Prompt | PinCallback | None
PasswordSource = str | bytes | os.PathLike | <has open()> | <has read()> | Callable[[], str | bytes | None] | None
A backend's pin= takes a PinSource: the PIN; KEYSTORE_PROMPT; a
callback, which decides per request how to get the PIN (prompt, read a vault,
cache, decline) and returns it, KEYSTORE_PROMPT, or None to decline; or
None, meaning no PIN. The callback is consulted only when the keystore
actually needs the user, and every time it does: purpose="list" (logging
in to see a token's private keys; declining just lists what is visible),
"sign" (before signing; declining fails the signature), "per-signature"
(the key demands the PIN for every signature, CKA_ALWAYS_AUTHENTICATE). It
runs on the thread doing the TLS handshake; an exception it raises fails that
handshake (or the listing) and propagates from identities(). A callback
returning anything else raises TypeError (the message never includes the
value). PinRequest never holds a PIN.
Loaders take a PasswordSource for encrypted keys and PKCS#12 bundles: the
password as str (UTF-8) or bytes (a str is always the password itself,
never a path); a file holding it (os.PathLike, or an object with open()
or read(): its first line, without the line ending, as OpenSSL's -passin
file: reads it); or a callable. A file or callable is consulted only if the
key turns out to be encrypted (a callable returning None fails with
CredentialError). OpenSSL's terminal passphrase prompt is never used.
def ask(request: sslcontext.PinRequest):
if request.purpose == "list":
return None # list without logging in
return getpass(f"PIN for {request.token} ({request.certificate}): ")
backend = PKCS11Backend(pin=ask)
Keystores (sslcontext.backends)
class Backend(ABC):
name: ClassVar[str]
def identities(self) -> Iterator[Identity] # abstract
def find_identities(self, *, thumbprint=None, subject=None, issuer=None,
label=None, valid=True) -> list[Identity]
def find_identity(self, **query) -> Identity
def close(self) -> None
# context manager: `with backend: ...`
A backend's constructor does no I/O. identities() yields every entry whose
private key is usable; it raises KeystoreError when the keystore cannot be
read. find_identities keeps entries matching every criterion given, newest
expiry first: thumbprint is SHA-1 or SHA-256 hex (case, spaces, colons
ignored); subject/issuer match a case-insensitive substring of the RFC
4514 name; label equals the keystore's own entry name (case-insensitive);
valid=True keeps only certificates valid now. find_identity returns the
first of those (the renewed certificate wins) or raises
IdentityNotFoundError. Identities stay usable after the backend is closed.
def names() -> list[str]
def get(name: str) -> type[Backend] # ValueError naming registered ones
def register_backend(name: str, backend: type[Backend] | str) -> None
Third-party backends register under the entry-point group
sslcontext.backends (name = "module:Class"). Names are lower case; a
taken name raises ValueError.
| Name | Class | Keystore |
|---|---|---|
cng |
CNGBackend |
Windows certificate stores; keys in CNG providers (software, TPM, smart cards and virtual smart cards via minidrivers) or legacy CryptoAPI CSPs |
pkcs11 |
PKCS11Backend |
PKCS#11 tokens: smart cards (real or virtual) through OpenSC, HSMs, TPMs (tpm2-pkcs11), SoftHSM; any OS |
keychain |
KeychainBackend |
the macOS Keychain: the user's keychains, and smart cards through CryptoTokenKit |
class CNGBackend(Backend):
def __init__(self, store: str = "MY", location: str = "current_user", *,
pin: PinSource = None)
store is a system store name ("MY" = Personal). location is
"current_user" or "local_machine" (else ValueError). Keys are opened
and used silently; only when Windows reports that a key needs the user
(NTE_SILENT_CONTEXT, a smart card without its PIN) is pin consulted
(purpose="sign"): a PIN is set on the key (SmartCardPin) and the
signature retried silently; KEYSTORE_PROMPT reopens the key with Windows'
dialogs allowed; no PIN fails with KeystoreError (errno.EACCES). A wrong
PIN is never retried. label is the certificate's Windows friendly name. The chain is what Windows can build from its stores without
network access, root excluded. A key that only a legacy CryptoAPI CSP can use
has signer.supports_pss == False (TLS 1.2 only); legacy software CSP keys
open through CNG and support TLS 1.3. On a non-Windows system, identities()
raises KeystoreError (errno.ENOSYS).
import sslcontext
from sslcontext.backends import CNGBackend
ctx = sslcontext.create_context()
sslcontext.load_identity(ctx, CNGBackend().find_identity(subject="alice"))
class PKCS11Backend(Backend):
def __init__(self, module: str | os.PathLike | None = None, *,
token: str | None = None, slot: int | None = None,
pin: PinSource = None)
def tokens(self) -> list[str]
module is the PKCS#11 library; default: SSLCONTEXT_PKCS11_MODULE, else
p11-kit's proxy (p11-kit-proxy.so, every module the system configured) on
Linux, else OpenSC (opensc-pkcs11.dll/.so). Missing → KeystoreError
(errno.ENOENT). token keeps only the token with that label or serial
number, slot only that slot id; uninitialised tokens are skipped.
tokens() lists the matching token labels.
pin is a PinSource. It is consulted only when the token is not
already logged in for this process (and for every signature by an
always-authenticate key). KEYSTORE_PROMPT logs in through the reader's PIN
pad (CKF_PROTECTED_AUTHENTICATION_PATH); on a token without one it fails
at once ("has no PIN pad"). With no PIN, a login-required token lists the
certificates that carry a CKA_ID (keys may be hidden until login) and its
signers fail at once with KeystoreError ("none was given"). A wrong PIN
raises KeystoreError naming CKR_PIN_INCORRECT and is not retried; errno
holds the CKR_* value.
An identity is a CKO_CERTIFICATE (X.509) and the CKO_PRIVATE_KEY with the
same CKA_ID; label is the certificate's CKA_LABEL; the chain is the
issuers found among the token's own certificates, root excluded. Signing uses
CKM_RSA_PKCS (over a DigestInfo), CKM_RSA_PKCS_PSS and CKM_ECDSA;
signer.supports_pss reports whether the token offers PSS. A key with
CKA_ALWAYS_AUTHENTICATE (a PIV card's signature key, slot 9C) gets
C_Login(CKU_CONTEXT_SPECIFIC) before every signature. Each signer keeps
one session, serialised by a lock. When the card is removed and reinserted,
the next signature finds the token again by label and serial number, opens
a new session and logs in again; it fails with KeystoreError if the token
is still absent.
Modules are loaded once per path, initialised with CKF_OS_LOCKING_OK, and
never finalised.
from sslcontext.backends import PKCS11Backend
backend = PKCS11Backend("/usr/lib64/pkcs11/opensc-pkcs11.so",
pin=lambda request: getpass(f"PIN for {request.token}: "))
sslcontext.load_identity(ctx, backend.find_identity(label="Certificate for PIV Authentication"))
class KeychainBackend(Backend):
def __init__(self, keychain: str | os.PathLike | None = None, *,
pin: PinSource = None)
Searches the user's keychain search list (login, System) and the
identities smart cards expose (CryptoTokenKit), or only the keychain file
keychain (it must exist and be unlocked; missing: KeystoreError,
errno.ENOENT). label is the certificate's subject summary (usually its
common name); the chain is what macOS builds without network access, root
excluded. Signing goes through SecKeyCreateSignature (RSA PKCS#1 v1.5 and
PSS, the salt as long as the digest; ECDSA), silently first: only when macOS
reports that the key needs the user (errSecInteractionNotAllowed: an
access prompt, a smart-card PIN, Touch ID) is pin consulted
(purpose="sign"): KEYSTORE_PROMPT retries with macOS's own dialog
allowed; anything else fails at once with KeystoreError (errno.EACCES),
since a PIN value cannot be handed to the Keychain. On another system,
identities() raises KeystoreError (errno.ENOSYS).
from sslcontext.backends import KeychainBackend
sslcontext.load_identity(ctx, KeychainBackend().find_identity(subject="alice"))
Issuing certificates (sslcontext.issuing)
class CertificateAuthority:
def __init__(self, certificate: bytes | str, key: bytes | str, password: PasswordSource = None)
@classmethod
def create(cls, common_name: str, *, permitted: Iterable[str] = (), days: float = 3650,
key_type: str = "ec") -> CertificateAuthority
certificate: bytes # CA certificate, PEM: what clients trust
der: bytes
info: CertificateInfo
def private_key_pem(self, password: str | bytes | None = None) -> bytes
def issue(self, names: Iterable[str], *, days: float = 30, key_type: str = "ec",
common_name: str | None = None) -> IssuedCertificate
def server_context(self, names: Iterable[str], **create_context_options) -> ssl.SSLContext
@dataclass(frozen=True)
class IssuedCertificate:
certificate: bytes # leaf, PEM
key: bytes # PKCS#8 PEM, unencrypted (not in repr)
chain: bytes # the CA certificate, PEM
info: CertificateInfo # property
def load_into(self, context: ssl.SSLContext) -> None
Keys and certificates come from the interpreter's own OpenSSL (1.1.1 and
3.x); no third-party package. create makes a self-signed CA (critical
basicConstraints CA:TRUE, pathlen:0, keyUsage keyCertSign,cRLSign, SKI).
permitted adds critical RFC 5280 name constraints: DNS names, where
corp.example, .corp.example and *.corp.example all mean the domain and
its subdomains, and IP networks (10.0.0.0/8); a name that cannot be a
constraint (re:..., a wildcard inside a label) raises ValueError.
key_type is "ec" (P-256) or "rsa" (2048), else ValueError. Loading
from PEM raises CredentialError for malformed input or a key that is not
the certificate's. issue makes a leaf with a fresh key for DNS names and
IP addresses (SAN), serverAuth+clientAuth, SKI and AKI, valid from an
hour ago, so strict verification (the default from Python 3.13) accepts it;
ValueError for no names. private_key_pem(password) exports the CA key,
AES-256 encrypted when a password is given; store it as a secret. The CA
key lives in memory; keystore-held CA keys are not supported.
Command line (sslcontext, extra cli)
sslcontext list [SOURCE] [--all] [--format text|json] [--json]
sslcontext show [SOURCE] QUERY
sslcontext probe [SOURCE] URL [--cafile F] [-k] [--ask] [--no-policy] [--timeout S] [--format text|json] [--json]
SOURCE: [-b system|cng|pkcs11|<registered>] [--store MY] [--location current_user|local_machine]
[--module PATH] [--token LABEL] [--pin none|ask|keystore]
list prints the identities whose key is usable (valid ones unless
--all): SHA-1, expiry, key, client/no-client (whether a browser would
offer it for client authentication), subject, issuer. show prints one in
full as JSON, by SHA-1/SHA-256 (case, spaces, colons ignored) or a subject
substring; none matching exits 1. probe connects with system_context()
over the chosen sources (https://host[:port]/path also sends HEAD and
prints the status line; host[:port] is TLS only) and reports the server's
certificate, each certificate request (accepted issuers, the candidates,
what was sent); a failed connection is reported and exits 1. --ask picks
on the terminal when several fit; --pin ask reads a PIN with getpass
(never for listing), --pin keystore uses KEYSTORE_PROMPT; the default
never waits. -b system is system_sources(). Exit codes: 0 done, 1 failed
(or the extra is missing, or standard output was closed), 2 wrong invocation
(an unknown backend, a scheme other than https), 130 Ctrl-C.
sslcontext.cli.main(argv=None) -> int runs it in
process; importing sslcontext.cli needs no duho.
Test helpers (sslcontext.testing)
class SoftwareSigner:
def __init__(self, key: bytes | str, password: PasswordSource = None)
calls: list[tuple[str, Padding | None, int | None]]
def sign(self, digest, *, hash, padding=None, salt_length=None) -> bytes
def software_identity(certificate, key, password=None) -> Identity
SoftwareSigner is a Signer over an in-memory PEM/DER key, signing with
OpenSSL; calls records every (hash, padding, salt_length) it was asked
for. software_identity wraps a PEM certificate (+ chain) and key into an
Identity. Meant for tests and as the reference a keystore signer must
match.
Exceptions
All in sslcontext.exceptions, re-exported from the root.
| Class | Bases | Raised when |
|---|---|---|
SSLContextError |
Exception |
base of everything below |
UnsupportedOpenSSLError |
SSLContextError |
the interpreter's TLS library cannot be driven: OpenSSL < 1.1.1, a static _ssl that hides its symbols, or an unrecognised SSLContext or LibreSSL structure layout |
CredentialError |
SSLContextError, ValueError |
malformed certificate/key, wrong password, key/certificate mismatch |
IdentityNotFoundError |
SSLContextError, LookupError |
no keystore entry matched a query |
KeystoreError |
SSLContextError, OSError |
the keystore refused or failed an operation |
SigningError |
SSLContextError |
a signer failed during a handshake |
Environment variables
| Variable | Sets | Default |
|---|---|---|
SSLCONTEXT_PKCS11_MODULE |
the PKCS#11 module PKCS11Backend() loads when module is not given |
p11-kit proxy on Linux, OpenSC elsewhere |
Gotchas
sslcontextdrives the TLS library that the running interpreter'ssslmodule loaded, through ctypes: OpenSSL 1.1.1 or later, or LibreSSL 2.8 (Apple's/usr/bin/python3). On LibreSSL there is no TLS 1.3 (so no RSA-PSS), and aClientCertificateSelectorsees the server's CA names but not its signature algorithms, sends the leaf without its chain, and is not asked when the context already holds a loaded identity.- A selector, like a
choosecallback, runs inside the handshake on the thread doing it (in asyncio, the event loop's); the first request lists the sources, which can open a keystore. Listing is cached forcache_ttl; callidentities()beforehand to warm it. - PIN callbacks, and with
KEYSTORE_PROMPTthe keystore's own dialogs, run inside the TLS handshake on the thread doing it; in asyncio a callback that waits for a person blocks the event loop. Without either, nothing waits. - PKCS#11 login state belongs to the process and the token: once any session logged in, later logins succeed without checking the PIN, until every session on that token is closed.
SSL_CTX_check_private_keycompares public keys only, so anIdentitywhose signer holds a different private key than the certificate loads fine and fails at the peer, which rejects the signature.- On first
load_identity,sslcontextregisters an OpenSSL provider namedsslcontextin the default library context (OpenSSL 3), or creates an engine with idsslcontext(OpenSSL 1.1). Either serves only the keysload_identitycreates; every other key keeps using OpenSSL's own code. - PKCS#12 bundles encrypted with legacy algorithms (RC2, 3DES; common in old
.pfxexports) need OpenSSL'slegacyprovider, which many Python builds do not ship; such a bundle fails withCredentialError. Re-export it with AES, or convert it to PEM.