Skip to main content

boring-ssl2

BoringSSL as the engine behind ssl2: one shared library with brotli inside, built from a pinned commit with two small patches and bound with ctypes. A ClientHello model goes out byte for byte or is refused with the reason, over TCP or, for a QUIC stack, through the handshake and packet protection of RFC 9001. Requires Python 3.14 and ssl2.

uv add boring-ssl2

HTTPS over the system trust store

from boring_ssl2 import ClientContext
from ssl2 import NextProtocol, TLSClientConfiguration

client = ClientContext(TLSClientConfiguration(inner_protocols=(NextProtocol.HTTP1,)))
connection = client.connect(("www.python.org", 443), timeout=10)
connection.send(b"HEAD / HTTP/1.1\r\nHost: www.python.org\r\nConnection: close\r\n\r\n")
print(connection.recv(4096).split(b"\r\n", 1)[0])  # b'HTTP/1.1 200 OK'
print(connection.negotiated_tls_version, connection.cipher(), connection.negotiated_protocol())  # TLSVersion.TLSv1_3 CipherSuite.TLS_AES_128_GCM_SHA256 NextProtocol.HTTP1
connection.close()

A ClientHello, byte for byte

from boring_ssl2 import ClientContext
from ssl2 import (
    JA4,
    CertificateCompressionAlgorithm,
    CipherSuite,
    ClientHelloBuilder,
    ClientHelloParser,
    ECPointFormat,
    ExtensionType,
    NamedGroup,
    NextProtocol,
    ProtocolVersion,
    PskKeyExchangeMode,
    SignatureScheme,
    TLSClientConfiguration,
    WantReadError,
)

profile = (
    ClientHelloBuilder()
    .supported_versions(ProtocolVersion.TLS_1_3, ProtocolVersion.TLS_1_2)
    .cipher_suites(
        CipherSuite.TLS_AES_128_GCM_SHA256,
        CipherSuite.TLS_AES_256_GCM_SHA384,
        CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
        CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
        CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
    )
    .extensions(
        ExtensionType.server_name,
        ExtensionType.extended_main_secret,
        ExtensionType.renegotiation_info,
        ExtensionType.supported_groups,
        ExtensionType.ec_point_formats,
        ExtensionType.session_ticket,
        ExtensionType.application_layer_protocol_negotiation,
        ExtensionType.status_request,
        ExtensionType.signature_algorithms,
        ExtensionType.signed_certificate_timestamp,
        ExtensionType.key_share,
        ExtensionType.psk_key_exchange_modes,
        ExtensionType.supported_versions,
        ExtensionType.compress_certificate,
        ExtensionType.application_settings,
        ExtensionType.encrypted_client_hello,
    )
    .supported_groups(NamedGroup.X25519MLKEM768, NamedGroup.x25519, NamedGroup.secp256r1, NamedGroup.secp384r1)
    .key_shares(NamedGroup.X25519MLKEM768, NamedGroup.x25519)
    .ec_point_formats(ECPointFormat.uncompressed)
    .signature_algorithms(
        SignatureScheme.ecdsa_secp256r1_sha256,
        SignatureScheme.rsa_pss_rsae_sha256,
        SignatureScheme.rsa_pkcs1_sha256,
        SignatureScheme.ecdsa_secp384r1_sha384,
        SignatureScheme.rsa_pss_rsae_sha384,
        SignatureScheme.rsa_pkcs1_sha384,
        SignatureScheme.rsa_pss_rsae_sha512,
        SignatureScheme.rsa_pkcs1_sha512,
    )
    .application_protocols(NextProtocol.H2, NextProtocol.HTTP1)
    .application_settings(NextProtocol.H2)
    .certificate_compression(CertificateCompressionAlgorithm.brotli)
    .psk_key_exchange_modes(PskKeyExchangeMode.psk_dhe_ke)
    .build()
)
client = ClientContext(TLSClientConfiguration(client_hello=profile))

buffer = client.create_buffer("tls.peet.ws")
try:
    buffer.do_handshake()
except WantReadError:
    pass
print(ClientHelloParser.parse(buffer.process_outgoing(65536)) == profile)  # True
print(JA4.of(profile).text)  # t13d1516h2_8daaf6152771_d8a2da3f94cd, what tls.peet.ws reports for this connection

connection = client.connect(("tls.peet.ws", 443), timeout=10)  # or proxy=Proxy.parse("socks5h://user:secret@127.0.0.1:1080")
print(connection.negotiated_protocol())  # NextProtocol.H2
connection.close()

grease=True and permute_extensions=True on the model give Chrome's randomized order. What BoringSSL cannot send is refused when the context is built, with the reason:

from dataclasses import replace

from ssl2 import ConfigurationError, NamedGroup

try:
    ClientContext(TLSClientConfiguration(client_hello=replace(profile, supported_groups=(NamedGroup.ffdhe2048, *profile.supported_groups))))
except ConfigurationError as refusal:
    print(refusal)  # BoringSSL does not support the group ffdhe2048

An older client

Clients built on BoringSSL before August 2026, Android among them, pad a ClientHello of 256 to 511 bytes up to 512 with the padding extension. The second patch keeps that behaviour available: list padding last and set legacy_padding, and the padding comes and goes with the size of the hello, as it did on those clients.

from boring_ssl2 import ClientContext
from ssl2 import CipherSuite, ClientHelloBuilder, ClientHelloParser, ECPointFormat, ExtensionType, NamedGroup, NextProtocol, ProtocolVersion, SignatureScheme, TLSClientConfiguration, WantReadError

android = (
    ClientHelloBuilder()
    .supported_versions(ProtocolVersion.TLS_1_2)
    .cipher_suites(
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
        CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
        CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
        CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
        CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
        CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
    )
    .extensions(
        ExtensionType.server_name,
        ExtensionType.extended_main_secret,
        ExtensionType.renegotiation_info,
        ExtensionType.supported_groups,
        ExtensionType.ec_point_formats,
        ExtensionType.signature_algorithms,
        ExtensionType.application_layer_protocol_negotiation,
        ExtensionType.padding,
    )
    .supported_groups(NamedGroup.x25519, NamedGroup.secp256r1, NamedGroup.secp384r1)
    .ec_point_formats(ECPointFormat.uncompressed)
    .signature_algorithms(
        SignatureScheme.ecdsa_secp256r1_sha256,
        SignatureScheme.rsa_pss_rsae_sha256,
        SignatureScheme.rsa_pkcs1_sha256,
        SignatureScheme.ecdsa_secp384r1_sha384,
        SignatureScheme.rsa_pss_rsae_sha384,
        SignatureScheme.rsa_pkcs1_sha384,
        SignatureScheme.rsa_pss_rsae_sha512,
        SignatureScheme.rsa_pkcs1_sha512,
        SignatureScheme.rsa_pkcs1_sha1,
    )
    .application_protocols(NextProtocol.H2, NextProtocol.HTTP1)
    .legacy_padding()
    .build()
)
client = ClientContext(TLSClientConfiguration(client_hello=android))
for host in ("www.example.com", "a-longer-server-name-with-many-labels.in-the-middle-of-a-domain-that-goes-on.and-another-label-to-pass-the-size.example.org"):
    buffer = client.create_buffer(host)
    try:
        buffer.do_handshake()
    except WantReadError:
        pass
    record = buffer.process_outgoing(65536)
    print(len(record) - 5, ExtensionType.padding in ClientHelloParser.parse(record).extensions)  # 162 False, then 512 True

asyncio

import asyncio

from boring_ssl2 import ClientContext
from ssl2 import NextProtocol, TLSClientConfiguration, TLSStream


async def main() -> None:
    client = ClientContext(TLSClientConfiguration(inner_protocols=(NextProtocol.HTTP1,)))
    async with asyncio.timeout(10):
        stream = await TLSStream.connect(client, ("www.python.org", 443))
    await stream.write(b"HEAD / HTTP/1.1\r\nHost: www.python.org\r\nConnection: close\r\n\r\n")
    print((await stream.read(4096)).split(b"\r\n", 1)[0])  # b'HTTP/1.1 200 OK'
    await stream.close()


asyncio.run(main())

Session resumption

from boring_ssl2 import ClientContext
from ssl2 import NextProtocol, SessionCache, TLSClientConfiguration

cache = SessionCache()
client = ClientContext(TLSClientConfiguration(inner_protocols=(NextProtocol.HTTP1,)), session_cache=cache)
for _ in range(2):
    connection = client.connect(("www.python.org", 443), timeout=10)
    connection.send(b"HEAD / HTTP/1.1\r\nHost: www.python.org\r\nConnection: close\r\n\r\n")
    connection.recv(4096)
    print(connection.buffer.session_reused)  # False, then True
    connection.close()

A model that lists pre_shared_key last needs a session_cache; the first hello goes out without it and the resumed hello equals the model. key_log="keys.txt" on ClientContext appends every secret of its TCP and QUIC handshakes in the NSS key log format, which Wireshark reads.

Server

from boring_ssl2 import ServerContext
from ssl2 import Certificate, PrivateKey, SigningChain, TLSServerConfiguration

chain = SigningChain((Certificate.from_file("server.pem"), PrivateKey.from_file("server.key")))
server = ServerContext(TLSServerConfiguration(certificate_chain=(chain,)))
listener = server.connect(("127.0.0.1", 8443))
listener.listen(5)
connection, peer = listener.accept()
connection.send(b"echo: " + connection.recv(1024))
connection.close()

Several chains in certificate_chain are picked by SNI. A trust_store on the server configuration demands a client certificate, which the client passes as certificate_chain. The server offers brotli, zstd and zlib certificate compression to clients that ask for it.

Sans-IO

import socket

from boring_ssl2 import ClientContext
from ssl2 import TLSClientConfiguration, WantReadError

client = ClientContext(TLSClientConfiguration())
tcp = socket.create_connection(("www.python.org", 443))
buffer = client.create_buffer("www.python.org")
while True:
    try:
        buffer.do_handshake()
        break
    except WantReadError:
        tcp.sendall(buffer.process_outgoing(65536))
        buffer.process_incoming(tcp.recv(65536))
print(buffer.negotiated_tls_version, buffer.cipher())  # TLSVersion.TLSv1_3 CipherSuite.TLS_AES_128_GCM_SHA256

Your own BoringSSL

The wheel carries a build of the pinned commit. A build of your own, another commit or a patched checkout, is a path:

from boring_ssl2 import Library

library = Library()  # the bundled build; Library(path="/opt/tls/libboring_ssl2.so") is yours
print(library.source_revision)  # ed9209cff8b5a2fbc6c10050f29b3355048bda85

ClientContext(configuration, library=library) and ServerContext(configuration, library=library) use it. Functions bind when first called, so a build that lacks one fails at the feature that needs it and names the symbol, not at import.

The same CMakeLists.txt builds any version. The commit, its archive hash and the patch directory are cache variables; a checkout on disk skips the download and the patches:

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --target boring_ssl2
cmake --install build --prefix src       # writes src/boring_ssl2/libboring_ssl2.so, .dylib or .dll

cmake -S . -B build -DBORINGSSL_REVISION=<commit> -DBORINGSSL_ARCHIVE_SHA256=<sha256 of the GitHub archive>
cmake -S . -B build -DBORINGSSL_SOURCE_DIR=/path/to/boringssl

The patches in patches/boringssl add SSL_CTX_set1_extension_order for the model's extension order and SSL_CTX_set_legacy_padding for the padding of older clients. They come from commits on a checkout and apply to the download in file name order; a build without them lacks those two features and nothing else.

The wheels on PyPI carry the library for Linux (glibc 2.28, x86_64 and aarch64), macOS 11 (arm64 and x86_64) and Windows x64, with the licenses of BoringSSL and brotli next to it in boring_ssl2/licenses. On Termux, pkg install cmake ninja clang git and the three cmake commands above build it on the device, after which uv pip install . packages what was built. For an iOS app the publish workflow leaves the arm64 device and simulator builds as artifacts; the app bundles one as a framework and passes its path to Library.

Metadata

Release files for boring-ssl2 2.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for boring-ssl2 2.0.3
File Actions
boring_ssl2-2.0.3-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details for boring_ssl2-2.0.3-py3-none-win_amd64.whl
boring_ssl2-2.0.3-py3-none-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.26+ x86-64, Linux glibc 2.28+ x86-64 Details for boring_ssl2-2.0.3-py3-none-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
boring_ssl2-2.0.3-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.26+ ARM64, Linux glibc 2.28+ ARM64 Details for boring_ssl2-2.0.3-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
boring_ssl2-2.0.3-py3-none-macosx_11_0_x86_64.whl Python 3 none macOS 11.0+ x86-64 Details for boring_ssl2-2.0.3-py3-none-macosx_11_0_x86_64.whl
boring_ssl2-2.0.3-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details for boring_ssl2-2.0.3-py3-none-macosx_11_0_arm64.whl

Total release size: 8.9 MB

Release files / boring_ssl2-2.0.3-py3-none-win_amd64.whl

Download URL boring_ssl2-2.0.3-py3-none-win_amd64.whl
Size 1.5 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
ce3ca1c5ce3d9f3e22c08f15653fe824b5b82a5332c2991cc36bee1d6bcc896a
BLAKE2b-256 checksum
How to use checksums
64a6127e506bb9508594a3aac056d3927b05e6b6c9946aa2b0244d31d379e4ce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / boring_ssl2-2.0.3-py3-none-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl

Download URL boring_ssl2-2.0.3-py3-none-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
Size 2.0 MB
Tags Linux glibc 2.26+ x86-64 Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
24f382a626ec3b5d0854c6d238ee6880292f73c034c6e5e5a3c68439d738d7a9
BLAKE2b-256 checksum
How to use checksums
17277b4e00a826d461449cf46dc5c85ba97e7a944a2d24be2d07ac95d754a6ae
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / boring_ssl2-2.0.3-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl

Download URL boring_ssl2-2.0.3-py3-none-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl
Size 1.9 MB
Tags Linux glibc 2.26+ ARM64 Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
43e7967032e1d62019d631613dbabdd8c281d969a7af24ae4a579a01c2192637
BLAKE2b-256 checksum
How to use checksums
d9c5cf877e5e4017037dfdf378e15e17af2fd040b13e6f07307a458c3425db0c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / boring_ssl2-2.0.3-py3-none-macosx_11_0_x86_64.whl

Download URL boring_ssl2-2.0.3-py3-none-macosx_11_0_x86_64.whl
Size 1.8 MB
Tags Python 3 macOS 11.0+ x86-64
SHA-256 checksum
How to use checksums
3f9b0bb4a16e26cfc0aa1d71adfce78d9325409f908c620d53eb87e5ea613526
BLAKE2b-256 checksum
How to use checksums
c542c9c31fb628e1ace702d3872670da87cc1e73f7d4cd5479212185338c4ed3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Release files / boring_ssl2-2.0.3-py3-none-macosx_11_0_arm64.whl

Download URL boring_ssl2-2.0.3-py3-none-macosx_11_0_arm64.whl
Size 1.7 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
d619243a09a64bf23288b5b09e838f81f1b5d21d896067e4d234ba00e88160a4
BLAKE2b-256 checksum
How to use checksums
53398861e7755c52e235f5502eede2879e92048087565ad7b17c866132261489
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

Security for boring-ssl2 2.0.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 5 files in this release have been uploaded with provenance metadata.

Select from 5 files

Attestation for boring_ssl2-2.0.3-py3-none-win_amd64.whl

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 31, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page