Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,39 @@ API. In most cases, Python methods of such objects directly match the OpenSSL fu

These classes should be considered internal.

### OpenSSL Version Support

Nassl supports multiple OpenSSL versions:

- **Legacy OpenSSL (1.0.2)**: Available via `nassl._nassl_legacy`
- **Modern OpenSSL (1.1.1)**: Available via `nassl._nassl`
- **OpenSSL 3.x**: Available via `nassl._nassl3` (NEW!)

To check which versions are available:

```python
from nassl import get_openssl_versions, has_openssl3_support

print("Available versions:", get_openssl_versions())
print("OpenSSL 3 support:", has_openssl3_support())
```

To use the OpenSSL 3 client:

```python
from nassl.openssl3_ssl_client import OpenSSL3SslClient

if OpenSSL3SslClient.is_available():
ssl_client = OpenSSL3SslClient()
# Use ssl_client...
```

### Building OpenSSL 3 Support

To build with OpenSSL 3 support:

$ invoke build.openssl3
$ invoke build.nassl

Why another SSL library?
------------------------
Expand Down
69 changes: 69 additions & 0 deletions build_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,62 @@ def exe_path(self) -> Path:
return self.src_path / "apps" / "openssl"


class OpenSSL3BuildConfig(OpenSslBuildConfig):
@property
def _openssl_git_tag(self) -> str:
return "openssl-3.5.1"

_OPENSSL_CONF_CMD = (
"perl Configure {target} zlib no-zlib-dynamic no-shared enable-rc5 enable-md2 enable-gost "
"enable-cast enable-idea enable-ripemd enable-mdc2 --with-zlib-include={zlib_include_path} "
"--with-zlib-lib={zlib_lib_path} enable-weak-ssl-ciphers enable-tls1_3 {extra_args} no-async"
)

def _run_build_steps(self, ctx: Context) -> None:
if self.platform in [
SupportedPlatformEnum.WINDOWS_32,
SupportedPlatformEnum.WINDOWS_64,
]:
ctx.run("nmake clean", warn=True)
ctx.run("nmake")
else:
return super()._run_build_steps(ctx)

@property
def libcrypto_path(self) -> Path:
if self.platform in [
SupportedPlatformEnum.WINDOWS_32,
SupportedPlatformEnum.WINDOWS_64,
]:
return self.src_path / "libcrypto.lib"
else:
return self.src_path / "libcrypto.a"

@property
def libssl_path(self) -> Path:
if self.platform in [
SupportedPlatformEnum.WINDOWS_32,
SupportedPlatformEnum.WINDOWS_64,
]:
return self.src_path / "libssl.lib"
else:
return self.src_path / "libssl.a"

@property
def include_path(self) -> Path:
return self.src_path / "include"

@property
def exe_path(self) -> Path:
if self.platform in [
SupportedPlatformEnum.WINDOWS_32,
SupportedPlatformEnum.WINDOWS_64,
]:
return self.src_path / "apps" / "openssl.exe"
else:
return self.src_path / "apps" / "openssl"


class ZlibBuildConfig(BuildConfig):
@property
def src_tar_gz_url(self) -> str:
Expand Down Expand Up @@ -457,6 +513,18 @@ def build_modern_openssl(ctx, do_not_clean=False):
print("OPENSSL MODERN: All done")


@task
def build_openssl3(ctx, do_not_clean=False):
print("OPENSSL 3: Starting...")
ssl3_cfg = OpenSSL3BuildConfig(CURRENT_PLATFORM)
if not do_not_clean:
ssl3_cfg.clean()
ssl3_cfg.fetch_source()
zlib_cfg = ZlibBuildConfig(CURRENT_PLATFORM)
ssl3_cfg.build(ctx, zlib_lib_path=zlib_cfg.libz_path, zlib_include_path=zlib_cfg.include_path)
print("OPENSSL 3: All done")


@task
def build_nassl(ctx):
"""Build the nassl C extension."""
Expand All @@ -480,6 +548,7 @@ def build_deps(ctx, do_not_clean=False):
build_zlib(ctx, do_not_clean)
build_legacy_openssl(ctx, do_not_clean)
build_modern_openssl(ctx, do_not_clean)
build_openssl3(ctx, do_not_clean)


@task
Expand Down
38 changes: 38 additions & 0 deletions nassl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,40 @@
__author__ = "Alban Diquet"
__version__ = "5.3.0"


def _detect_available_openssl_versions():
"""Detect which OpenSSL versions are available."""
available_versions = []

# Try legacy OpenSSL
try:
import nassl._nassl_legacy # noqa: F401
available_versions.append("legacy")
except ImportError:
pass

# Try modern OpenSSL (1.1.1)
try:
import nassl._nassl # noqa: F401
available_versions.append("modern")
except ImportError:
pass

# Try OpenSSL 3
try:
import nassl._nassl3 # noqa: F401
available_versions.append("openssl3")
except ImportError:
pass

return available_versions


def get_openssl_versions():
"""Get a list of available OpenSSL versions."""
return _detect_available_openssl_versions()


def has_openssl3_support():
"""Check if OpenSSL 3 support is available."""
return "openssl3" in _detect_available_openssl_versions()
10 changes: 10 additions & 0 deletions nassl/_nassl/nassl.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ static struct PyModuleDef moduledef =

#ifdef LEGACY_OPENSSL
"_nassl_legacy",
#elif defined(OPENSSL3)
"_nassl3",
#else
"_nassl",
#endif
Expand All @@ -77,6 +79,8 @@ static struct PyModuleDef moduledef =

#ifdef LEGACY_OPENSSL
PyMODINIT_FUNC PyInit__nassl_legacy(void)
#elif defined(OPENSSL3)
PyMODINIT_FUNC PyInit__nassl3(void)
#else
PyMODINIT_FUNC PyInit__nassl(void)
#endif
Expand Down Expand Up @@ -124,7 +128,13 @@ PyMODINIT_FUNC PyInit__nassl(void)
#endif

state = GETSTATE(module);
#ifdef LEGACY_OPENSSL
state->error = PyErr_NewException("nassl._nassl_legacy.Error", NULL, NULL);
#elif defined(OPENSSL3)
state->error = PyErr_NewException("nassl._nassl3.Error", NULL, NULL);
#else
state->error = PyErr_NewException("nassl._nassl.Error", NULL, NULL);
#endif
if (state->error == NULL)
{
Py_DECREF(module);
Expand Down
13 changes: 13 additions & 0 deletions nassl/_nassl3.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Any

SSL_CTX: Any
SSL: Any
BIO: Any
X509: Any
X509_STORE_CTX: Any
OCSP_RESPONSE: Any
OpenSSLError: Any
SslError: Any
WantReadError: Any
WantX509LookupError: Any
SSL_SESSION: Any
177 changes: 177 additions & 0 deletions nassl/openssl3_ssl_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""SSL client implementation using OpenSSL 3.x."""
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given the experimental project status, maybe it makes since to move this into modern ssl and move the current modern ssl to legacy (but this would remove support for the existing legacy client)


import socket
from pathlib import Path
from typing import List, Any, Tuple, Optional

try:
from nassl import _nassl3 as nassl3_module
from nassl._nassl3 import WantReadError, OpenSSLError, WantX509LookupError
_OPENSSL3_AVAILABLE = True
except ImportError:
_OPENSSL3_AVAILABLE = False
nassl3_module = None # type: ignore

# Define enums locally to avoid circular imports - using same values as ssl_client.py
class OpenSslVersionEnum:
SSLV23 = 0
SSLV2 = 1
SSLV3 = 2
TLSV1 = 3
TLSV1_1 = 4
TLSV1_2 = 5
TLSV1_3 = 6

class OpenSslVerifyEnum:
NONE = 0
PEER = 1

from nassl.ephemeral_key_info import EphemeralKeyInfo


class OpenSSL3UnavailableError(Exception):
"""Raised when OpenSSL 3 functionality is requested but not available."""
pass


class OpenSSL3SslClient:
"""SSL client implementation using OpenSSL 3.x.

This class provides an interface to OpenSSL 3.x functionality through
the _nassl3 C extension. It offers similar functionality to the regular
SslClient but uses the newer OpenSSL 3 API.
"""

def __init__(
self,
ssl_version: OpenSslVersionEnum = OpenSslVersionEnum.TLSV1_2,
ssl_verify: OpenSslVerifyEnum = OpenSslVerifyEnum.NONE,
ssl_verify_locations: Optional[Path] = None,
client_certchain_file: Optional[Path] = None,
client_key_file: Optional[Path] = None,
client_key_type: int = 1, # PEM format
client_key_password: str = "",
ignore_client_authentication_requests: bool = False,
) -> None:
"""Initialize the OpenSSL 3 SSL client.

Args:
ssl_version: The SSL/TLS version to use
ssl_verify: SSL verification mode
ssl_verify_locations: Path to CA certificates for verification
client_certchain_file: Path to client certificate chain file
client_key_file: Path to client private key file
client_key_type: Type of the client key file (PEM or DER)
client_key_password: Password for the client key file
ignore_client_authentication_requests: Whether to ignore client auth requests

Raises:
OpenSSL3UnavailableError: If OpenSSL 3 support is not available
"""
if not _OPENSSL3_AVAILABLE:
raise OpenSSL3UnavailableError(
"OpenSSL 3 support is not available. Make sure the _nassl3 extension was built."
)

# Store configuration
self._ssl_version = ssl_version
self._ssl_verify = ssl_verify
self._ssl_verify_locations = ssl_verify_locations
self._client_certchain_file = client_certchain_file
self._client_key_file = client_key_file
self._client_key_type = client_key_type
self._client_key_password = client_key_password
self._ignore_client_authentication_requests = ignore_client_authentication_requests

# Initialize SSL context and connection objects
self._ssl_ctx = None
self._ssl = None
self._socket = None

self._init_ssl_ctx()

def _init_ssl_ctx(self) -> None:
"""Initialize the SSL context with OpenSSL 3."""
# Create SSL context - use the enum value directly since our local enums are just integers
ssl_version_value = self._ssl_version if isinstance(self._ssl_version, int) else self._ssl_version.value
self._ssl_ctx = nassl3_module.SSL_CTX(ssl_version_value)

# Set verification mode
ssl_verify_value = self._ssl_verify if isinstance(self._ssl_verify, int) else self._ssl_verify.value
self._ssl_ctx.set_verify(ssl_verify_value)

# Set CA certificate locations if provided
if self._ssl_verify_locations:
self._ssl_ctx.load_verify_locations(str(self._ssl_verify_locations))

# Set client certificate and key if provided
if self._client_certchain_file:
self._ssl_ctx.use_certificate_chain_file(str(self._client_certchain_file))

if self._client_key_file:
self._ssl_ctx.use_PrivateKey_file(
str(self._client_key_file),
self._client_key_type
)

def set_underlying_socket(self, sock: socket.socket) -> None:
"""Set the underlying socket for the SSL connection."""
self._socket = sock

# Create SSL object
self._ssl = nassl3_module.SSL(self._ssl_ctx)

# Create BIO and set it to the SSL object
sock_bio = nassl3_module.BIO()
sock_bio.set_sock(sock)
self._ssl.set_bio(sock_bio)

def do_handshake(self) -> None:
"""Perform the SSL handshake."""
if not self._ssl:
raise ValueError("Socket must be set before performing handshake")
self._ssl.do_handshake()

def write(self, data: bytes) -> int:
"""Write data to the SSL connection."""
if not self._ssl:
raise ValueError("Socket must be set before writing")
return self._ssl.write(data)

def read(self, size: int = 1024) -> bytes:
"""Read data from the SSL connection."""
if not self._ssl:
raise ValueError("Socket must be set before reading")
return self._ssl.read(size)

def get_peer_certificate(self):
"""Get the peer's certificate."""
if not self._ssl:
raise ValueError("Socket must be set before getting peer certificate")
return self._ssl.get_peer_certificate()

def get_peer_cert_chain(self):
"""Get the peer's certificate chain."""
if not self._ssl:
raise ValueError("Socket must be set before getting peer certificate chain")
return self._ssl.get_peer_cert_chain()

def get_current_cipher(self):
"""Get the current cipher being used."""
if not self._ssl:
raise ValueError("Socket must be set before getting current cipher")
return self._ssl.get_current_cipher()

def shutdown(self) -> None:
"""Shutdown the SSL connection."""
if self._ssl:
try:
self._ssl.shutdown()
except OpenSSLError:
# Ignore shutdown errors
pass

@staticmethod
def is_available() -> bool:
"""Check if OpenSSL 3 support is available."""
return _OPENSSL3_AVAILABLE
Loading
Loading