Skip to content

API Reference

Every public name, generated from the package's own docstrings.

The same surface in prose, with the bounds and the gotchas, is the API header that ships inside the installed package, sslcontext/AGENTS.md, readable with importlib.resources.files("sslcontext") and printed in the Guide of this site.

Client identities from OS keystores in a standard :class:ssl.SSLContext.

VerifyLike = Union[bool, str, 'os.PathLike[str]', None] module-attribute

True/False, or a CA bundle file or hashed CA directory to trust.

HASHES = ('sha1', 'sha224', 'sha256', 'sha384', 'sha512') module-attribute

Hash names a signer may be asked to sign for, as :mod:hashlib spells them.

BytesOrText = Union[bytes, bytearray, memoryview, str] module-attribute

PEM text as str, or bytes holding PEM or DER.

PasswordSource = Union[str, bytes, 'os.PathLike[str]', Any, Callable[[], Union[str, bytes, None]], None] module-attribute

A password (str or bytes), a file holding it (os.PathLike, or an object with open() or read(): its first line, without the line ending), a callable returning one (or None to decline), or None.

PinCallback = Callable[[PinRequest], Union[str, Prompt, None]] module-attribute

Returns the PIN, :data:KEYSTORE_PROMPT, or None to decline.

PinSource = Union[str, Prompt, PinCallback, None] module-attribute

A PIN, :data:KEYSTORE_PROMPT, a :data:PinCallback, or None (no PIN: fail instead of asking anyone).

CertificateLike = Union[str, bytes, bytearray, memoryview, 'os.PathLike[str]', Any] module-attribute

One certificate source: PEM text (a str holding a PEM header), PEM or DER bytes, a path (str without a PEM header, or os.PathLike), an object with open() (pathlib.Path and its kin, zip paths, package resources), a readable file object, or a cryptography certificate.

CertificatesLike = Union[CertificateLike, Iterable[CertificateLike]] module-attribute

One source, or a list or tuple of them.

KeyLike = Union[str, bytes, bytearray, memoryview, 'os.PathLike[str]', Any] module-attribute

A private key from the same sources, or a cryptography private key.

Chooser = Callable[[CertificateRequest, List[Identity]], Optional[Identity]] module-attribute

Picks one of the candidates, or None to send no certificate.

IdentitySource = Union['Backend', Identity, Callable[[], Iterable[Identity]]] module-attribute

A keystore backend, one identity, or a callable returning identities.

CertificateInfo(subject, issuer, serial, not_before, not_after, sha1, sha256, key_type, key_bits) dataclass

What a certificate says about itself, decoded by OpenSSL.

subject instance-attribute

RFC 4514 distinguished name, e.g. CN=alice,O=Example.

serial instance-attribute

Serial number in upper-case hexadecimal.

not_after instance-attribute

Validity bounds, timezone-aware UTC.

sha1 instance-attribute

SHA-1 thumbprint of the DER certificate, upper-case hex (Windows' "Thumbprint").

key_type instance-attribute

"rsa", "ec" or "other".

valid_at(when=None)

Whether when (default: now) lies inside the validity period.

Source code in src/sslcontext/_identity.py
77
78
79
80
def valid_at(self, when: Optional[datetime.datetime] = None) -> bool:
    """Whether ``when`` (default: now) lies inside the validity period."""
    when = when or datetime.datetime.now(datetime.timezone.utc)
    return self.not_before <= when <= self.not_after

Identity(certificate, signer, chain=(), label='') dataclass

A certificate, its chain, and a signer for its private key.

certificate and each entry of chain are DER bytes; chain runs from the issuer of certificate upward and may omit the root.

info cached property

The decoded certificate. Raises CredentialError if it is malformed.

Padding

Bases: Enum

RSA signature padding. ECDSA signatures have none.

Signer

Bases: Protocol

Signs a digest with a private key the caller cannot see.

Implemented by every keystore backend, and by anything else that can sign: a remote HSM, a cloud KMS, a test key.

A signer may also have a supports_pss attribute. When it is False for an RSA key, :func:load_identity keeps the context's own signatures to PKCS#1 v1.5, which limits that identity to TLS 1.2.

sign(digest, *, hash, padding=None, salt_length=None)

Return the signature of digest, already hashed with hash.

hash is one of :data:HASHES. For an RSA key padding is set; with :attr:Padding.PSS, salt_length is the salt length in bytes and MGF1 uses hash. For an EC key padding is None and the result is a DER-encoded ECDSA signature. Raising any exception fails the handshake.

Called from the thread performing the TLS handshake, with OpenSSL mid-operation: a signer must not use the same ssl.SSLContext.

Source code in src/sslcontext/_identity.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def sign(
    self,
    digest: bytes,
    *,
    hash: str,
    padding: Optional[Padding] = None,
    salt_length: Optional[int] = None,
) -> bytes:
    """Return the signature of ``digest``, already hashed with ``hash``.

    ``hash`` is one of :data:`HASHES`. For an RSA key ``padding`` is set;
    with :attr:`Padding.PSS`, ``salt_length`` is the salt length in bytes
    and MGF1 uses ``hash``. For an EC key ``padding`` is ``None`` and the
    result is a DER-encoded ECDSA signature. Raising any exception fails
    the handshake.

    Called from the thread performing the TLS handshake, with OpenSSL
    mid-operation: a signer must not use the same ``ssl.SSLContext``.
    """
    ...

PinRequest(backend, token, certificate, purpose, keystore_prompt) dataclass

What a PIN callback is told. It never contains a PIN.

backend instance-attribute

The backend asking: "cng" or "pkcs11".

token instance-attribute

The token's label (PKCS#11), or the key's container name (CNG).

certificate instance-attribute

Subject of the certificate whose key needs the PIN; "" when the request is not about one key (logging in to list a token).

purpose instance-attribute

"list": logging in to see private keys; declining only hides them. "sign": logging in before a signature; declining fails it. "per-signature": the key demands the PIN for every signature (CKA_ALWAYS_AUTHENTICATE, a PIV card's signature key).

keystore_prompt instance-attribute

Whether returning :data:KEYSTORE_PROMPT can work here (the token has a PIN pad, or Windows can show its dialog).

Prompt

Bases: Enum

Sentinels a PIN source may give instead of a PIN.

KEYSTORE = 'keystore' class-attribute instance-attribute

Let the keystore ask the user itself: Windows' PIN dialog, a reader's PIN pad. Waits for the user, so it is never the default.

AutoSelectRule(pattern, issuer=None, subject=None) dataclass

Pick a certificate without asking, as Chrome's and Edge's AutoSelectCertificateForUrls policy does.

pattern is a host pattern: example.com (that host), [*.]example.com (it and every subdomain), *.example.com (the subdomains), * (any host). A scheme, port or path in it is accepted and ignored: the request knows only the server name. issuer and subject filter the certificate: a mapping of RFC 4514 attributes to exact values ({"CN": "Corp CA"}, as the policy's ISSUER/SUBJECT), or a string matched as a case-insensitive substring of the whole name.

CertificateRequest(host, authorities, authorities_der, key_types, rsa_pss_required, tls_version) dataclass

What a server asked for when it requested a client certificate.

host instance-attribute

The server name the client sent (SNI); None when it sent none.

authorities instance-attribute

RFC 4514 names of the CAs the server accepts; empty means any.

key_types instance-attribute

"rsa" and/or "ec": key types the server's signature algorithms allow.

rsa_pss_required instance-attribute

An RSA key must sign RSA-PSS (TLS 1.3, or a server offering no PKCS#1).

tls_version instance-attribute

"TLSv1.3", "TLSv1.2", ...

ClientCertificateSelector(sources, *, choose=None, rules=(), browser_policy=False, remember=True, cache_ttl=60.0)

Answers a server's certificate request with an identity from sources.

Candidates are the identities a browser would offer: valid now, usable for client authentication (no extended key usage, clientAuth or anyExtendedKeyUsage; digitalSignature when key usage is present), of a key type the server's signature algorithms allow (an RSA key that cannot sign PSS is left out where PSS is required), and, when the server names CAs, issued under one of them (the leaf's issuer or any certificate of its chain). Newest expiry first.

The choice, in order: the identity remembered for the host, the first of rules matching the host with a matching candidate, the same for the browsers' policies when browser_policy is true (see :func:browser_policies), choose(request, candidates), and, with no choose, the only candidate when there is exactly one. Otherwise no certificate is sent and the server decides.

Sources are listed again after cache_ttl seconds. A source that fails is logged and skipped. An exception from choose (or a result that is not one of the candidates) fails that handshake.

Source code in src/sslcontext/_select.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def __init__(
    self,
    sources: Iterable[IdentitySource],
    *,
    choose: Optional[Chooser] = None,
    rules: Iterable[AutoSelectRule] = (),
    browser_policy: bool = False,
    remember: bool = True,
    cache_ttl: float = 60.0,
) -> None:
    self.sources = list(sources)
    self.choose = choose
    self.rules = tuple(rules)
    self.browser_policy = browser_policy
    self.remember = remember
    self.cache_ttl = cache_ttl
    self._lock = threading.RLock()
    self._listed: List[Identity] = []
    self._listed_at: Optional[float] = None
    self._policies: Tuple[AutoSelectRule, ...] = ()
    self._policies_at: Optional[float] = None
    self._prepared: Dict[str, _Prepared] = {}
    self._remembered: Dict[str, str] = {}
    self._finalizer = weakref.finalize(self, _release, self._prepared)

identities()

Every identity the sources hold, listed at most every cache_ttl.

Source code in src/sslcontext/_select.py
219
220
221
222
223
224
225
226
def identities(self) -> List[Identity]:
    """Every identity the sources hold, listed at most every ``cache_ttl``."""
    with self._lock:
        now = time.monotonic()
        if self._listed_at is None or now - self._listed_at > self.cache_ttl:
            self._listed = list(self._list())
            self._listed_at = now
        return list(self._listed)

policies()

The browsers' rules (empty unless browser_policy), cached.

Source code in src/sslcontext/_select.py
242
243
244
245
246
247
248
249
250
251
252
253
def policies(self) -> Tuple[AutoSelectRule, ...]:
    """The browsers' rules (empty unless ``browser_policy``), cached."""
    if not self.browser_policy:
        return ()
    from ._policy import browser_policies

    with self._lock:
        now = time.monotonic()
        if self._policies_at is None or now - self._policies_at > self.cache_ttl:
            self._policies = tuple(browser_policies())
            self._policies_at = now
        return self._policies

refresh()

List the sources and read the policies again on the next request.

Source code in src/sslcontext/_select.py
255
256
257
258
def refresh(self) -> None:
    """List the sources and read the policies again on the next request."""
    with self._lock:
        self._listed_at = self._policies_at = None

forget(host=None)

Forget the choice made for host, or for every host.

Source code in src/sslcontext/_select.py
260
261
262
263
264
265
266
def forget(self, host: Optional[str] = None) -> None:
    """Forget the choice made for ``host``, or for every host."""
    with self._lock:
        if host is None:
            self._remembered.clear()
        else:
            self._remembered.pop(host.casefold(), None)

candidates(request)

The identities a browser would offer for request, newest expiry first.

Source code in src/sslcontext/_select.py
298
299
300
301
302
303
304
305
306
307
308
309
310
def candidates(self, request: CertificateRequest) -> List[Identity]:
    """The identities a browser would offer for ``request``, newest expiry first."""
    now = datetime.datetime.now(datetime.timezone.utc)
    wanted = set(request.authorities_der)
    found = []
    for identity in self.identities():
        try:
            if self._offered(identity, request, now, wanted):
                found.append(identity)
        except Exception as error:  # noqa: BLE001 - one bad entry is skipped
            _log.debug("sslcontext: skipping %r: %s", identity, error)
    found.sort(key=lambda identity: identity.info.not_after, reverse=True)
    return found

select(request)

The identity to answer request with, or None for none.

Source code in src/sslcontext/_select.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
def select(self, request: CertificateRequest) -> Optional[Identity]:
    """The identity to answer ``request`` with, or ``None`` for none."""
    candidates = self.candidates(request)
    if not candidates:
        return None
    host = request.host.casefold() if request.host else None
    with self._lock:
        remembered = self._remembered.get(host) if host else None
    if remembered:
        for identity in candidates:
            if identity.info.sha256 == remembered:
                return identity
    chosen = self._by_rules(request, candidates)
    if chosen is None:
        if self.choose is not None:
            chosen = self.choose(request, list(candidates))
            if chosen is not None and not any(chosen is c for c in candidates):
                raise ValueError(
                    "choose() must return one of the candidates or None"
                )
        elif len(candidates) == 1:
            chosen = candidates[0]
        else:
            _log.info(
                "sslcontext: %d certificates fit %s and nothing picks one; "
                "sending none (add a rule or a choose callback)",
                len(candidates),
                request.host,
            )
    if chosen is not None and host and self.remember:
        with self._lock:
            self._remembered[host] = chosen.info.sha256
    return chosen

install(context)

Answer every certificate request on context from now on.

A client context only; it replaces a selector installed before. An identity loaded with :func:~sslcontext.load_identity is still sent when this selector picks none. context keeps the selector alive.

Source code in src/sslcontext/_select.py
378
379
380
381
382
383
384
385
386
387
388
def install(self, context: ssl.SSLContext) -> None:
    """Answer every certificate request on ``context`` from now on.

    A client context only; it replaces a selector installed before. An
    identity loaded with :func:`~sslcontext.load_identity` is still sent
    when this selector picks none. ``context`` keeps the selector alive.
    """
    from ._openssl import openssl
    from ._openssl._certcb import install_handler

    install_handler(openssl(), context, self._answer)

CredentialError

Bases: SSLContextError, ValueError

A certificate or key is malformed, encrypted with another password, or does not match its certificate.

IdentityNotFoundError

Bases: SSLContextError, LookupError

No keystore entry matched the query.

KeystoreError

Bases: SSLContextError, OSError

The keystore refused or failed an operation; errno holds its code.

SigningError

Bases: SSLContextError

The signer failed while OpenSSL was performing a handshake.

SSLContextError

Bases: Exception

Base class for every error this package raises.

UnsupportedOpenSSLError

Bases: SSLContextError

The interpreter's OpenSSL cannot be driven by this package.

LibreSSL, OpenSSL older than 1.1.1, and a statically linked _ssl that hides its symbols all raise this.

create_context(purpose='client', *, verify=None, cafile=None, capath=None, cadata=None, ca=None, certificate=None, key=None, password=None, identity=None, minimum_version=None, maximum_version=None, alpn=None)

A context for purpose "client" or "server".

verify decides whether the peer's certificate is checked. None means the purpose's default: a client verifies the server, a server does not ask for a client certificate. True turns checking on (a server then requires a client certificate), False turns it off (a client also stops checking the host name). A path is shorthand for trusting that CA bundle file or hashed directory, as in requests.

Trust comes from ca/cafile/capath/cadata when any is given, and from the platform's default store otherwise. ca takes the CA certificates themselves or where they are, in any form :data:CertificatesLike names, one or a list.

certificate (leaf first, then its chain; one source or a list), key and password load the context's own certificate as :func:load_pem does; identity (an :class:Identity) as :func:load_identity does. SSLKEYLOGFILE is honoured as by :func:ssl.create_default_context.

Source code in src/sslcontext/_context.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def create_context(
    purpose: str = "client",
    *,
    verify: VerifyLike = None,
    cafile: str | os.PathLike[str] | None = None,
    capath: str | os.PathLike[str] | None = None,
    cadata: str | bytes | None = None,
    ca: Optional[CertificatesLike] = None,
    certificate: Optional[CertificatesLike] = None,
    key: Optional[KeyLike] = None,
    password: Any = None,
    identity: Any = None,
    minimum_version: ssl.TLSVersion | None = None,
    maximum_version: ssl.TLSVersion | None = None,
    alpn: Iterable[str] | None = None,
) -> ssl.SSLContext:
    """A context for ``purpose`` ``"client"`` or ``"server"``.

    ``verify`` decides whether the peer's certificate is checked. ``None``
    means the purpose's default: a client verifies the server, a server does
    not ask for a client certificate. ``True`` turns checking on (a server
    then requires a client certificate), ``False`` turns it off (a client
    also stops checking the host name). A path is shorthand for trusting that
    CA bundle file or hashed directory, as in ``requests``.

    Trust comes from ``ca``/``cafile``/``capath``/``cadata`` when any is
    given, and from the platform's default store otherwise. ``ca`` takes the
    CA certificates themselves or where they are, in any form
    :data:`CertificatesLike` names, one or a list.

    ``certificate`` (leaf first, then its chain; one source or a list),
    ``key`` and ``password`` load the context's own certificate as
    :func:`load_pem` does; ``identity`` (an :class:`Identity`) as
    :func:`load_identity` does. ``SSLKEYLOGFILE`` is honoured as by
    :func:`ssl.create_default_context`.
    """
    try:
        ssl_purpose = _PURPOSES[purpose]
    except KeyError:
        raise ValueError(
            f"purpose must be 'client' or 'server', not {purpose!r}"
        ) from None
    if verify is not None and not isinstance(verify, (bool, str, os.PathLike)):
        raise TypeError(f"verify must be a bool or a path, not {type(verify).__name__}")
    if isinstance(verify, (str, os.PathLike)):
        if cafile is not None or capath is not None:
            raise ValueError("pass a CA path as verify or as cafile/capath, not both")
        if os.path.isdir(verify):
            capath = verify
        else:
            cafile = verify
        verify = True

    der = b""
    if ca is not None:
        if cadata is not None:
            raise ValueError("pass CA certificates as ca or as cadata, not both")
        pem, der = pem_and_der(read_sources(ca, "ca"))
        # cadata takes PEM text or DER bytes, not both in one call.
        cadata, der = (pem, der) if pem else (der, b"")
    context = ssl.create_default_context(
        ssl_purpose, cafile=cafile, capath=capath, cadata=cadata
    )
    if der:
        context.load_verify_locations(cadata=der)
    if purpose == "client":
        if verify is False:
            context.check_hostname = False
            context.verify_mode = ssl.CERT_NONE
    elif verify:
        context.verify_mode = ssl.CERT_REQUIRED
    if minimum_version is not None:
        context.minimum_version = minimum_version
    if maximum_version is not None:
        context.maximum_version = maximum_version
    if alpn is not None:
        context.set_alpn_protocols(list(alpn))
    if certificate is not None:
        from ._load import load_pem

        load_pem(context, certificate, key, password)
    elif key is not None:
        raise ValueError("key needs its certificate")
    if identity is not None:
        from ._load import load_identity

        load_identity(context, identity)
    return context

load_identity(context, identity)

Make a keystore :class:Identity the context's certificate and key.

The private key stays wherever identity.signer keeps it: OpenSSL asks the signer for each handshake signature. The signer is held until context is garbage-collected.

Raises :class:CredentialError for a malformed certificate, :class:UnsupportedOpenSSLError when the interpreter's OpenSSL cannot host the signer, and TypeError for arguments of the wrong type.

Source code in src/sslcontext/_load.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def load_identity(context: ssl.SSLContext, identity: Identity) -> None:
    """Make a keystore :class:`Identity` the context's certificate and key.

    The private key stays wherever ``identity.signer`` keeps it: OpenSSL asks
    the signer for each handshake signature. The signer is held until
    ``context`` is garbage-collected.

    Raises :class:`CredentialError` for a malformed certificate,
    :class:`UnsupportedOpenSSLError` when the interpreter's OpenSSL cannot
    host the signer, and ``TypeError`` for arguments of the wrong type.
    """
    if not isinstance(identity, Identity):
        raise TypeError(f"expected Identity, got {type(identity).__name__}")
    lib = openssl()
    ssl_ctx_pointer(lib, context)  # type check before anything is allocated
    certificates = read_certificates(lib, identity.certificate)
    try:
        for link in identity.chain:
            certificates.extend(read_certificates(lib, link))
        public = lib.X509_get_pubkey(certificates[0])
        if not public:
            raise CredentialError(f"certificate has no public key: {drain_errors(lib)}")
        handle = _keys.remember(lib, identity.signer, public, identity.label)
        try:
            record = _keys.lookup(handle)
            key = make_private_key(lib, handle, record.key_type)
            try:
                install(lib, context, certificates[0], certificates[1:], key)
            finally:
                lib.EVP_PKEY_free(key)
            # LibreSSL negotiates TLS 1.2 without RSA-PSS: nothing to restrict.
            if (
                record.key_type == "rsa"
                and not lib.is_libressl
                and not getattr(identity.signer, "supports_pss", True)
            ):
                _sign_without_pss(lib, context)
        except BaseException:
            _keys.forget(lib, handle)
            raise
        weakref.finalize(context, _keys.forget, lib, handle)
    finally:
        for pointer in certificates:
            lib.X509_free(pointer)

load_pem(context, certificate, key=None, password=None)

Load a certificate, its chain and its private key.

certificate is the leaf followed by any chain certificates: PEM holding them in order, one DER certificate, or a list of sources (each PEM or DER) in that order, in any form :data:CertificatesLike names (text, bytes, paths, objects with open(), cryptography certificates). It may instead be one PKCS#12 bundle (.p12/.pfx), loaded as :func:load_pkcs12 loads it, with no key. key is a PEM or DER private key (DER: PKCS#8, encrypted or not, or traditional), likewise, or a cryptography private key; when omitted it is read from the first certificate source, which then holds both. password (a value, or a callable asked only if the key or bundle turns out encrypted) decrypts it. Nothing is written to disk.

Raises :class:CredentialError for malformed data, a wrong password, or a key that does not match the certificate; OSError when a named file cannot be read.

Source code in src/sslcontext/_load.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def load_pem(
    context: ssl.SSLContext,
    certificate: CertificatesLike,
    key: Optional[KeyLike] = None,
    password: PasswordSource = None,
) -> None:
    """Load a certificate, its chain and its private key.

    ``certificate`` is the leaf followed by any chain certificates: PEM
    holding them in order, one DER certificate, or a list of sources (each
    PEM or DER) in that order, in any form :data:`CertificatesLike` names
    (text, bytes, paths, objects with ``open()``, ``cryptography``
    certificates). It may instead be one PKCS#12 bundle (``.p12``/``.pfx``),
    loaded as :func:`load_pkcs12` loads it, with no ``key``. ``key`` is a
    PEM or DER private key (DER: PKCS#8, encrypted or not, or traditional),
    likewise, or a ``cryptography`` private key; when omitted it is read from
    the first certificate source, which then holds both. ``password`` (a
    value, or a callable asked only if the key or bundle turns out
    encrypted) decrypts it. Nothing is written to disk.

    Raises :class:`CredentialError` for malformed data, a wrong password, or a
    key that does not match the certificate; ``OSError`` when a named file
    cannot be read.
    """
    blobs = read_sources(certificate, "certificate")
    if any(is_pkcs12(blob) for blob in blobs):
        if len(blobs) != 1 or key is not None:
            raise ValueError(
                "a PKCS#12 bundle holds its own key and chain: pass it alone, "
                "without key"
            )
        load_pkcs12(context, blobs[0], password)
        return
    lib = openssl()
    key_data = blobs[0] if key is None else read_source(key, "key")
    certificates: list = []
    try:
        for blob in blobs:
            certificates.extend(read_certificates(lib, blob))
        private_key = read_key_with(lib, key_data, password)
        try:
            install(lib, context, certificates[0], certificates[1:], private_key)
        finally:
            lib.EVP_PKEY_free(private_key)
    finally:
        for pointer in certificates:
            lib.X509_free(pointer)

load_pkcs12(context, data, password=None)

Load the key, certificate and chain of a PKCS#12 (.p12/.pfx) bundle.

data is the bundle's bytes, its path, or an object with open() or read(). Raises :class:CredentialError for a malformed bundle or a wrong password, OSError when a named file cannot be read.

Source code in src/sslcontext/_load.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def load_pkcs12(
    context: ssl.SSLContext,
    data: Any,
    password: PasswordSource = None,
) -> None:
    """Load the key, certificate and chain of a PKCS#12 (``.p12``/``.pfx``) bundle.

    ``data`` is the bundle's bytes, its path, or an object with ``open()`` or
    ``read()``. Raises :class:`CredentialError` for a malformed bundle or a
    wrong password, ``OSError`` when a named file cannot be read.
    """
    data = read_source(data, "data")
    lib = openssl()
    if deferred(password):
        try:
            key, certificate, chain = read_pkcs12(lib, bytes(data), None)
        except CredentialError:
            given = resolve_password(password)
            if given is None:
                raise
            key, certificate, chain = read_pkcs12(lib, bytes(data), given)
    else:
        key, certificate, chain = read_pkcs12(
            lib, bytes(data), resolve_password(password)
        )
    try:
        install(lib, context, certificate, chain, key)
    finally:
        lib.EVP_PKEY_free(key)
        lib.X509_free(certificate)
        for pointer in chain:
            lib.X509_free(pointer)

browser_policies()

The AutoSelectCertificateForUrls rules set for Edge, Chrome and Chromium on this machine, machine-wide before per-user, in each browser's order. Empty when none is set; never raises for a policy that cannot be read.

Source code in src/sslcontext/_policy.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def browser_policies() -> List[AutoSelectRule]:
    """The ``AutoSelectCertificateForUrls`` rules set for Edge, Chrome and
    Chromium on this machine, machine-wide before per-user, in each
    browser's order. Empty when none is set; never raises for a policy that
    cannot be read."""
    if sys.platform == "win32":
        entries: Iterable = _registry_entries()
    elif sys.platform == "darwin":
        entries = _plist_entries()
    else:
        entries = _json_entries()
    try:
        return rules_from_policy(list(entries))
    except OSError as error:
        _log.debug("sslcontext: cannot read the browser policies: %s", error)
        return []

rules_from_policy(entries)

Rules from policy entries (JSON text or decoded objects), in order. A malformed entry is logged and skipped, as the browsers skip it.

Source code in src/sslcontext/_policy.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def rules_from_policy(entries: Iterable[Union[str, Mapping]]) -> List[AutoSelectRule]:
    """Rules from policy entries (JSON text or decoded objects), in order.
    A malformed entry is logged and skipped, as the browsers skip it."""
    rules = []
    for entry in entries:
        try:
            value = json.loads(entry) if isinstance(entry, str) else entry
            if not isinstance(value, Mapping) or not isinstance(
                value.get("pattern"), str
            ):
                raise ValueError("an entry needs a pattern")
            spec = value.get("filter") or {}
            if not isinstance(spec, Mapping):
                raise ValueError("filter must be an object")
            rules.append(
                AutoSelectRule(
                    value["pattern"],
                    issuer=_filter(spec.get("ISSUER")),
                    subject=_filter(spec.get("SUBJECT")),
                )
            )
        except ValueError as error:
            _log.debug("sslcontext: skipping policy entry %r: %s", entry, error)
    return rules

system_context(*, selector=None, pin=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)

A client context that behaves as the system's own clients do.

Servers are verified by the operating system (truststore: CryptoAPI on Windows, the Security framework on macOS, the system store elsewhere) on Python 3.10 and later, and by OpenSSL over the default store on 3.9. A verify, ca, cafile, capath or cadata means what it means to :func:create_context and replaces the system's trust.

A server's certificate request is answered by selector: by default :func:system_selector (pin=pin); False sends no certificate. A certificate (with key and password) or an identity is loaded as by :func:create_context and is then the one sent: the default selector is not installed unless selector asks for it. TLS 1.3 post-handshake requests are accepted. One context serves every server and may be shared between threads.

Source code in src/sslcontext/_system.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
def system_context(
    *,
    selector: Union[ClientCertificateSelector, bool, None] = None,
    pin: PinSource = None,
    verify: VerifyLike = None,
    ca: Optional[CertificatesLike] = None,
    cafile: Union[str, "os.PathLike[str]", None] = None,
    capath: Union[str, "os.PathLike[str]", None] = None,
    cadata: Union[str, bytes, None] = None,
    certificate: Optional[CertificatesLike] = None,
    key: Optional[KeyLike] = None,
    password: Any = None,
    identity: Any = None,
    minimum_version: Optional[ssl.TLSVersion] = None,
    maximum_version: Optional[ssl.TLSVersion] = None,
    alpn: Optional[Iterable[str]] = None,
) -> ssl.SSLContext:
    """A client context that behaves as the system's own clients do.

    Servers are verified by the operating system (``truststore``: CryptoAPI
    on Windows, the Security framework on macOS, the system store elsewhere)
    on Python 3.10 and later, and by OpenSSL over the default store on 3.9.
    A ``verify``, ``ca``, ``cafile``, ``capath`` or ``cadata`` means what it
    means to :func:`create_context` and replaces the system's trust.

    A server's certificate request is answered by ``selector``: by default
    :func:`system_selector` ``(pin=pin)``; ``False`` sends no certificate.
    A ``certificate`` (with ``key`` and ``password``) or an ``identity`` is
    loaded as by :func:`create_context` and is then the one sent: the
    default selector is not installed unless ``selector`` asks for it. TLS
    1.3 post-handshake requests are accepted. One context serves every
    server and may be shared between threads.
    """
    custom_trust = any(
        value is not None for value in (verify, ca, cafile, capath, cadata)
    )
    truststore_class = None if custom_trust else _truststore()
    if truststore_class is not None:
        context = truststore_class(ssl.PROTOCOL_TLS_CLIENT)
        if minimum_version is not None:
            context.minimum_version = minimum_version
        if maximum_version is not None:
            context.maximum_version = maximum_version
        if alpn is not None:
            context.set_alpn_protocols(list(alpn))
        if certificate is not None:
            from ._load import load_pem

            load_pem(context, certificate, key, password)
        elif key is not None:
            raise ValueError("key needs its certificate")
        if identity is not None:
            from ._load import load_identity

            load_identity(context, identity)
    else:
        context = create_context(
            "client",
            verify=verify,
            ca=ca,
            cafile=cafile,
            capath=capath,
            cadata=cadata,
            certificate=certificate,
            key=key,
            password=password,
            identity=identity,
            minimum_version=minimum_version,
            maximum_version=maximum_version,
            alpn=alpn,
        )
        if not custom_trust and sys.platform == "darwin":
            _load_macos_anchors(context)
    if ssl.HAS_TLSv1_3:  # read-only on a TLS library without it (LibreSSL 2.x)
        context.post_handshake_auth = True
    if selector is None:
        selector = certificate is None and identity is None
    if selector is True:
        selector = system_selector(pin=pin)
    if selector is not False:
        if not isinstance(selector, ClientCertificateSelector):
            raise TypeError(
                "selector must be a ClientCertificateSelector, True, False or None"
            )
        selector.install(context)
    return context

system_selector(*, pin=None, choose=None, rules=(), browser_policy=True, remember=True, cache_ttl=60.0)

A :class:ClientCertificateSelector over :func:system_sources, following the browsers' AutoSelectCertificateForUrls policy.

Source code in src/sslcontext/_system.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def system_selector(
    *,
    pin: PinSource = None,
    choose: Optional[Chooser] = None,
    rules: Iterable[AutoSelectRule] = (),
    browser_policy: bool = True,
    remember: bool = True,
    cache_ttl: float = 60.0,
) -> ClientCertificateSelector:
    """A :class:`ClientCertificateSelector` over :func:`system_sources`,
    following the browsers' ``AutoSelectCertificateForUrls`` policy."""
    return ClientCertificateSelector(
        system_sources(pin=pin),
        choose=choose,
        rules=rules,
        browser_policy=browser_policy,
        remember=remember,
        cache_ttl=cache_ttl,
    )

system_sources(*, pin=None)

The keystores a browser on this system offers client certificates from.

Windows: the user's Personal store (CNGBackend("MY")), which also holds the certificates of inserted smart cards. macOS: the Keychain (KeychainBackend(): the user's keychains and smart cards). Elsewhere: the PKCS#11 module :class:~sslcontext.backends.PKCS11Backend would load by default (p11-kit's proxy on Linux, every token the system configured), when one is installed. pin is given to each backend. Nothing is opened here.

Source code in src/sslcontext/_system.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def system_sources(*, pin: PinSource = None) -> List["Backend"]:
    """The keystores a browser on this system offers client certificates from.

    Windows: the user's Personal store (``CNGBackend("MY")``), which also
    holds the certificates of inserted smart cards. macOS: the Keychain
    (``KeychainBackend()``: the user's keychains and smart cards). Elsewhere:
    the PKCS#11 module :class:`~sslcontext.backends.PKCS11Backend` would load
    by default (p11-kit's proxy on Linux, every token the system configured),
    when one is installed. ``pin`` is given to each backend. Nothing is opened here.
    """
    if sys.platform == "win32":
        from .backends import CNGBackend

        return [CNGBackend("MY", "current_user", pin=pin)]
    if sys.platform == "darwin":
        from .backends import KeychainBackend

        return [KeychainBackend(pin=pin)]
    from .backends import PKCS11Backend
    from .backends._pkcs11 import default_module

    if default_module() is None:
        return []
    return [PKCS11Backend(pin=pin)]

Keystore backends

Keystores that hold identities: the contract, the registry, the built-ins.

A backend lists the identities (certificate + usable private key) in one keystore and finds one by a query. Built-in backends:

========== ================================ ===================================== name class keystore ========== ================================ ===================================== cng :class:CNGBackend Windows certificate stores (CNG/CAPI) pkcs11 :class:PKCS11Backend PKCS#11 tokens: smart cards, HSMs, TPMs keychain :class:KeychainBackend the macOS Keychain, smart cards included ========== ================================ =====================================

Third-party backends register under the entry-point group sslcontext.backends (name = "package.module:Class").

Backend

Bases: ABC

One keystore. Constructing it does no I/O; :meth:close releases it.

Subclasses implement :meth:identities; querying is shared. A backend is a context manager, and identities it returned stay usable after it is closed.

close()

Release what the backend holds open. Harmless to call twice.

Source code in src/sslcontext/backends/__init__.py
65
66
def close(self) -> None:
    """Release what the backend holds open. Harmless to call twice."""

identities() abstractmethod

Every identity in the keystore whose private key can be used.

Raises :class:~sslcontext.exceptions.KeystoreError when the keystore cannot be read.

Source code in src/sslcontext/backends/__init__.py
68
69
70
71
72
73
74
@abstractmethod
def identities(self) -> Iterator[Identity]:
    """Every identity in the keystore whose private key can be used.

    Raises :class:`~sslcontext.exceptions.KeystoreError` when the
    keystore cannot be read.
    """

find_identities(*, thumbprint=None, subject=None, issuer=None, label=None, valid=True)

The identities matching every criterion given, newest expiry first.

thumbprint is the certificate's SHA-1 or SHA-256 in hex (case, spaces and colons ignored). subject and issuer match a case-insensitive substring of the RFC 4514 name. label matches the keystore's own name for the entry exactly, ignoring case. valid keeps only certificates valid now.

Source code in src/sslcontext/backends/__init__.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def find_identities(
    self,
    *,
    thumbprint: Optional[str] = None,
    subject: Optional[str] = None,
    issuer: Optional[str] = None,
    label: Optional[str] = None,
    valid: bool = True,
) -> List[Identity]:
    """The identities matching every criterion given, newest expiry first.

    ``thumbprint`` is the certificate's SHA-1 or SHA-256 in hex (case,
    spaces and colons ignored). ``subject`` and ``issuer`` match a
    case-insensitive substring of the RFC 4514 name. ``label`` matches
    the keystore's own name for the entry exactly, ignoring case.
    ``valid`` keeps only certificates valid now.
    """
    wanted = _normalize_thumbprint(thumbprint) if thumbprint else None
    now = datetime.datetime.now(datetime.timezone.utc)
    found = []
    for identity in self.identities():
        info = identity.info
        if wanted and wanted not in (info.sha1, info.sha256):
            continue
        if subject and subject.casefold() not in info.subject.casefold():
            continue
        if issuer and issuer.casefold() not in info.issuer.casefold():
            continue
        if label is not None and label.casefold() != identity.label.casefold():
            continue
        if valid and not info.valid_at(now):
            continue
        found.append(identity)
    found.sort(key=lambda identity: identity.info.not_after, reverse=True)
    return found

find_identity(**query)

The best match for :meth:find_identities (**query).

Several matches resolve to the one that expires last, so a renewed certificate wins over the one it replaces. Raises :class:~sslcontext.exceptions.IdentityNotFoundError when nothing matches.

Source code in src/sslcontext/backends/__init__.py
112
113
114
115
116
117
118
119
120
121
122
123
124
def find_identity(self, **query: Any) -> Identity:
    """The best match for :meth:`find_identities` ``(**query)``.

    Several matches resolve to the one that expires last, so a renewed
    certificate wins over the one it replaces. Raises
    :class:`~sslcontext.exceptions.IdentityNotFoundError` when nothing
    matches.
    """
    found = self.find_identities(**query)
    if not found:
        criteria = ", ".join(f"{k}={v!r}" for k, v in query.items()) or "any"
        raise IdentityNotFoundError(f"no identity in {self!r} matches {criteria}")
    return found[0]

register_backend(name, backend)

Register backend (a class, or "module:Class") under name.

Names are lower case; registering a taken name raises ValueError.

Source code in src/sslcontext/backends/__init__.py
131
132
133
134
135
136
137
138
139
140
141
def register_backend(name: str, backend: Union[str, Type[Backend]]) -> None:
    """Register ``backend`` (a class, or ``"module:Class"``) under ``name``.

    Names are lower case; registering a taken name raises ``ValueError``.
    """
    if not name or name != name.lower():
        raise ValueError(f"backend names are lower case and non-empty: {name!r}")
    with _lock:
        if name in _registry or name in _BUILTINS:
            raise ValueError(f"a backend named {name!r} is already registered")
        _registry[name] = backend

names()

Every registered backend name, built-in and third-party.

Source code in src/sslcontext/backends/__init__.py
162
163
164
165
def names() -> List[str]:
    """Every registered backend name, built-in and third-party."""
    _load_entry_points()
    return sorted(set(_BUILTINS) | set(_registry))

get(name)

The backend class registered as name.

An unknown name raises ValueError naming the registered ones.

Source code in src/sslcontext/backends/__init__.py
168
169
170
171
172
173
174
175
176
177
178
179
180
def get(name: str) -> Type[Backend]:
    """The backend class registered as ``name``.

    An unknown name raises ``ValueError`` naming the registered ones.
    """
    _load_entry_points()
    target = _registry.get(name) or _BUILTINS.get(name)
    if target is None:
        raise ValueError(f"unknown backend {name!r}; registered: {', '.join(names())}")
    if isinstance(target, str):
        module, _, attribute = target.partition(":")
        target = getattr(importlib.import_module(module), attribute)
    return target

Issuing certificates

A small certificate authority: create or load one, issue server certificates.

Keys and certificates are made by the interpreter's own OpenSSL, the one the ssl module uses, so no third-party package is needed and the result works on OpenSSL 1.1.1 and 3.x alike. The CA's private key is held in memory; :meth:CertificateAuthority.private_key_pem exports it (optionally encrypted) for the caller to store.

Meant for local use: test servers, and proxies that terminate TLS for clients told to trust the CA. Restrict such a CA with permitted= (X.509 name constraints) so that, if its key leaks, it cannot vouch for other sites.

IssuedCertificate(certificate, key, chain=b'') dataclass

A leaf certificate and its key, as PEM.

chain holds the issuing CA's certificate. :meth:load_into installs all of it into a context.

info property

The decoded leaf certificate.

CertificateAuthority(certificate, key, password=None)

A CA whose certificate and private key are held in memory.

Construct from PEM (certificate and key; password decrypts an encrypted key) or with :meth:create. Raises CredentialError for malformed PEM or a key that is not the certificate's.

Source code in src/sslcontext/issuing.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def __init__(
    self,
    certificate: BytesOrText,
    key: BytesOrText,
    password: PasswordSource = None,
) -> None:
    self._lib = lib = openssl()
    self._lock = threading.Lock()
    certificates = read_certificates(lib, read_source(certificate, "certificate"))
    for extra in certificates[1:]:
        lib.X509_free(extra)
    self._x509 = certificates[0]
    self._key = 0
    self._key = read_key_with(lib, read_source(key, "key"), password)
    public = lib.X509_get_pubkey(self._x509)
    try:
        matches = (
            lib.EVP_PKEY_eq(public, self._key) == 1
            if lib.is_v3
            else (lib.EVP_PKEY_cmp(public, self._key) == 1)
        )
    finally:
        lib.EVP_PKEY_free(public)
    if not matches:
        raise CredentialError("the key does not belong to the CA certificate")
    self.certificate: bytes = _bio_bytes(lib, lib.PEM_write_bio_X509, self._x509)
    """The CA certificate, PEM: what clients must trust."""

certificate = _bio_bytes(lib, lib.PEM_write_bio_X509, self._x509) instance-attribute

The CA certificate, PEM: what clients must trust.

create(common_name, *, permitted=(), days=3650, key_type='ec') classmethod

A new self-signed CA.

permitted names what it may vouch for, as critical X.509 name constraints: DNS names (corp.example covers its subdomains; *.corp.example and .corp.example mean the same) and IP networks (10.0.0.0/8). Empty means unconstrained. key_type is "ec" (P-256) or "rsa" (2048-bit). ValueError for a name that cannot be a constraint.

Source code in src/sslcontext/issuing.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
@classmethod
def create(
    cls,
    common_name: str,
    *,
    permitted: Iterable[str] = (),
    days: float = 3650,
    key_type: str = "ec",
) -> "CertificateAuthority":
    """A new self-signed CA.

    ``permitted`` names what it may vouch for, as critical X.509 name
    constraints: DNS names (``corp.example`` covers its subdomains;
    ``*.corp.example`` and ``.corp.example`` mean the same) and IP
    networks (``10.0.0.0/8``). Empty means unconstrained. ``key_type``
    is ``"ec"`` (P-256) or ``"rsa"`` (2048-bit). ``ValueError`` for a
    name that cannot be a constraint.
    """
    subtrees = _permitted_entries(permitted)
    lib = openssl()
    key = _generate_key(lib, key_type)
    try:
        builder = _Builder(lib, key, days)
        try:
            builder.subject(common_name)
            extensions = [
                ("basicConstraints", "critical,CA:TRUE,pathlen:0"),
                ("keyUsage", "critical,keyCertSign,cRLSign,digitalSignature"),
                ("subjectKeyIdentifier", "hash"),
            ]
            if subtrees:
                extensions.append(
                    ("nameConstraints", "critical," + ",".join(subtrees))
                )
            builder.extensions(0, extensions)
            x509 = builder.sign(builder.x509, key)
        finally:
            builder.close()
        try:
            certificate = _bio_bytes(lib, lib.PEM_write_bio_X509, x509)
        finally:
            lib.X509_free(x509)
        key_pem = _bio_bytes(
            lib, lib.PEM_write_bio_PKCS8PrivateKey, key, None, None, 0, None, None
        )
    finally:
        lib.EVP_PKEY_free(key)
    return cls(certificate, key_pem)

private_key_pem(password=None)

The CA key as PKCS#8 PEM, AES-256 encrypted when password is given.

Source code in src/sslcontext/issuing.py
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def private_key_pem(self, password: Union[str, bytes, None] = None) -> bytes:
    """The CA key as PKCS#8 PEM, AES-256 encrypted when ``password`` is given."""
    lib = self._lib
    secret = resolve_password(password)
    cipher = lib.EVP_aes_256_cbc() if secret else None
    return _bio_bytes(
        lib,
        lib.PEM_write_bio_PKCS8PrivateKey,
        self._key,
        cipher,
        None,
        0,
        None,
        secret,
    )

issue(names, *, days=30, key_type='ec', common_name=None)

A server certificate for names (DNS names and IP addresses).

The leaf carries them as subject alternative names, serverAuth and clientAuth usage, and an authority key identifier, so strict X.509 verification (the default from Python 3.13) accepts it. A fresh key each time. ValueError for an empty names.

Source code in src/sslcontext/issuing.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
def issue(
    self,
    names: Iterable[str],
    *,
    days: float = 30,
    key_type: str = "ec",
    common_name: Optional[str] = None,
) -> IssuedCertificate:
    """A server certificate for ``names`` (DNS names and IP addresses).

    The leaf carries them as subject alternative names, serverAuth and
    clientAuth usage, and an authority key identifier, so strict X.509
    verification (the default from Python 3.13) accepts it. A fresh key
    each time. ``ValueError`` for an empty ``names``.
    """
    names = [str(n) for n in names]
    if not names:
        raise ValueError("issue() needs at least one name")
    lib = self._lib
    key = _generate_key(lib, key_type)
    try:
        builder = _Builder(lib, key, days)
        try:
            builder.subject(common_name or names[0])
            builder.extensions(
                self._x509,
                [
                    ("basicConstraints", "critical,CA:FALSE"),
                    ("keyUsage", "critical,digitalSignature,keyEncipherment"),
                    ("extendedKeyUsage", "serverAuth,clientAuth"),
                    ("subjectAltName", ",".join(_san_and_constraints(names))),
                    ("subjectKeyIdentifier", "hash"),
                    ("authorityKeyIdentifier", "keyid:always"),
                ],
            )
            with self._lock:
                x509 = builder.sign(self._x509, self._key)
        finally:
            builder.close()
        try:
            certificate = _bio_bytes(lib, lib.PEM_write_bio_X509, x509)
        finally:
            lib.X509_free(x509)
        key_pem = _bio_bytes(
            lib, lib.PEM_write_bio_PKCS8PrivateKey, key, None, None, 0, None, None
        )
    finally:
        lib.EVP_PKEY_free(key)
    return IssuedCertificate(certificate, key_pem, self.certificate)

server_context(names, **options)

A server context presenting a fresh certificate for names.

options go to :func:sslcontext.create_context (purpose is "server").

Source code in src/sslcontext/issuing.py
413
414
415
416
417
418
419
420
421
def server_context(self, names: Iterable[str], **options: object) -> ssl.SSLContext:
    """A server context presenting a fresh certificate for ``names``.

    ``options`` go to :func:`sslcontext.create_context` (purpose is
    ``"server"``).
    """
    context = create_context("server", **options)  # type: ignore[arg-type]
    self.issue(names).load_into(context)
    return context

Test helpers

Test helpers: a :class:Signer over an in-memory key.

:class:SoftwareSigner holds an ordinary private key and signs with OpenSSL, through the same :class:Signer contract a keystore backend implements. It exercises the whole keystore path without a keystore, and it is the reference for what a backend's sign must return.

SoftwareSigner(key, password=None)

Signs with a PEM or DER private key held in memory.

calls records (hash, padding, salt_length) for every signature, so a test can see what the TLS stack asked for.

Source code in src/sslcontext/testing.py
42
43
44
45
46
def __init__(self, key: BytesOrText, password: PasswordSource = None) -> None:
    self._lib = openssl()
    self._key = read_key_with(self._lib, read_source(key, "key"), password)
    self._lock = threading.Lock()
    self.calls: List[Tuple[str, Optional[Padding], Optional[int]]] = []

software_identity(certificate, key, password=None)

An :class:Identity for a PEM certificate (+ chain) and an in-memory key.

Source code in src/sslcontext/testing.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def software_identity(
    certificate: BytesOrText,
    key: BytesOrText,
    password: PasswordSource = None,
) -> Identity:
    """An :class:`Identity` for a PEM certificate (+ chain) and an in-memory key."""
    lib = openssl()
    pointers = read_certificates(lib, read_source(certificate, "certificate"))
    try:
        ders = [certificate_der(lib, pointer) for pointer in pointers]
    finally:
        for pointer in pointers:
            lib.X509_free(pointer)
    return Identity(ders[0], SoftwareSigner(key, password), tuple(ders[1:]), "software")