├── nassl ├── py.typed ├── __init__.py ├── _nassl │ ├── python_utils.h │ ├── nassl_BIO.h │ ├── nassl_errors.h │ ├── nassl_SSL_SESSION.h │ ├── nassl_X509.h │ ├── nassl_OCSP_RESPONSE.h │ ├── openssl_utils.h │ ├── nassl_SSL_CTX.h │ ├── nassl_SSL.h │ ├── python_utils.c │ ├── nassl_X509_STORE_CTX.h │ ├── openssl_utils.c │ ├── nassl.c │ ├── nassl_SSL_SESSION.c │ ├── nassl_BIO.c │ ├── nassl_X509.c │ ├── nassl_OCSP_RESPONSE.c │ ├── nassl_errors.c │ ├── nassl_X509_STORE_CTX.c │ └── nassl_SSL_CTX.c ├── _nassl.pyi ├── _nassl_legacy.pyi ├── ocsp_response.py ├── cert_chain_verifier.py ├── ephemeral_key_info.py ├── legacy_ssl_client.py └── ssl_client.py ├── tests ├── __init__.py ├── build_config_test.py ├── openssl_server │ ├── client-cert.pem │ ├── client-ca.pem │ ├── client-key.pem │ ├── server-self-signed-cert.pem │ ├── server-self-signed-key.pem │ └── __init__.py ├── X509_test.py ├── X509_STORE_CTX_test.py ├── ocsp_response_test.py ├── ephemeral_key_info_test.py ├── cert_chain_verifier_test.py ├── SSL_test.py ├── SSL_CTX_test.py └── ssl_client_test.py ├── .gitattributes ├── requirements-dev.txt ├── pyproject.toml ├── .gitignore ├── .github └── workflows │ ├── run_tests.yml │ ├── build_wheels_linux_aarch64.yml │ └── build_wheels.yml ├── sample_client.py ├── README.md ├── tasks.py └── setup.py /nassl/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nassl/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = "Alban Diquet" 2 | __version__ = "5.3.1" 3 | -------------------------------------------------------------------------------- /nassl/_nassl/python_utils.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | void *PyArg_ParseFilePath(PyObject *args, char **filePathOut); 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.a filter=lfs diff=lfs merge=lfs -text 2 | *.lib filter=lfs diff=lfs merge=lfs -text 3 | .sh text eol=lf 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | mypy==1.14 2 | invoke>=2,<3 3 | pytest>=8,<9 4 | twine 5 | pytest-cov 6 | ruff==0.8.4 7 | setuptools 8 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 120 3 | 4 | [tool.mypy] 5 | python_version = "3.9" 6 | ignore_missing_imports = true 7 | strict_optional = true 8 | disallow_untyped_defs = true 9 | -------------------------------------------------------------------------------- /nassl/_nassl.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | SSL_CTX: Any 4 | SSL: Any 5 | BIO: Any 6 | X509: Any 7 | X509_STORE_CTX: Any 8 | OCSP_RESPONSE: Any 9 | OpenSSLError: Any 10 | SslError: Any 11 | WantReadError: Any 12 | WantX509LookupError: Any 13 | SSL_SESSION: Any 14 | -------------------------------------------------------------------------------- /nassl/_nassl_legacy.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | SSL_CTX: Any 4 | SSL: Any 5 | BIO: Any 6 | X509: Any 7 | X509_STORE_CTX: Any 8 | OCSP_RESPONSE: Any 9 | OpenSSLError: Any 10 | SslError: Any 11 | WantReadError: Any 12 | WantX509LookupError: Any 13 | SSL_SESSION: Any 14 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_BIO.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // nassl.BIO_Pair Python class 4 | typedef struct { 5 | PyObject_HEAD 6 | BIO *bio; 7 | } nassl_BIO_Object; 8 | 9 | // Type needs to be accessible to nassl_SSL.c 10 | extern PyTypeObject nassl_BIO_Type; 11 | 12 | void module_add_BIO(PyObject* m); 13 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_errors.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | extern PyObject *nassl_OpenSSLError_Exception; // Needed by nassl_X509.c 7 | 8 | PyObject* raise_OpenSSL_error(void); 9 | PyObject* raise_OpenSSL_ssl_error(SSL *ssl, int returnValue); 10 | int module_add_errors(PyObject* m); 11 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_SSL_SESSION.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef struct { 4 | PyObject_HEAD 5 | SSL_SESSION *sslSession; // OpenSSL SSL_SESSION C struct 6 | } nassl_SSL_SESSION_Object; 7 | 8 | // Type needs to be accessible to nassl_SSL.c 9 | extern PyTypeObject nassl_SSL_SESSION_Type; 10 | 11 | void module_add_SSL_SESSION(PyObject* m); 12 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_X509.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // nassl.X509 Python class 4 | typedef struct { 5 | PyObject_HEAD 6 | X509 *x509; // OpenSSL X509 C struct 7 | } nassl_X509_Object; 8 | 9 | // Type needs to be accessible to nassl_SSL.c 10 | extern PyTypeObject nassl_X509_Type; 11 | 12 | void module_add_X509(PyObject* m); 13 | 14 | PyObject* stackOfX509ToPyList(STACK_OF(X509) *certChain); 15 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_OCSP_RESPONSE.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef struct { 4 | PyObject_HEAD 5 | OCSP_RESPONSE *ocspResp; // OpenSSL OCSP_RESPONSE C struct 6 | STACK_OF(X509) *peerCertChain; // Certificate chain to help verify 7 | } nassl_OCSP_RESPONSE_Object; 8 | 9 | // Type needs to be accessible to nassl_SSL.c 10 | extern PyTypeObject nassl_OCSP_RESPONSE_Type; 11 | 12 | void module_add_OCSP_RESPONSE(PyObject* m); 13 | -------------------------------------------------------------------------------- /nassl/_nassl/openssl_utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "nassl_errors.h" 7 | 8 | // Takes an XXX_print() function and a pointer to the structure to be printed 9 | // Returns a Python string 10 | // Used by nassl_X509.c and nassl_SSL_SESSION.c 11 | PyObject* generic_print_to_string(int (*openSslPrintFunction)(BIO *fp, const void *a), const void *dataStruct); 12 | 13 | 14 | PyObject *bioToPyString(BIO *memBio); 15 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_SSL_CTX.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // nassl.SSL_CTX Python class 4 | typedef struct { 5 | PyObject_HEAD 6 | SSL_CTX *sslCtx; // OpenSSL SSL_CTX C struct 7 | char *pkeyPasswordBuf; // Buffer where the passcode to unlock the private key will be stored 8 | int ignoreClientCertRequests; // continue even if client certificate is missing 9 | } nassl_SSL_CTX_Object; 10 | 11 | // Type needs to be accessible to nassl_SSL.c 12 | extern PyTypeObject nassl_SSL_CTX_Type; 13 | 14 | void module_add_SSL_CTX(PyObject* m); 15 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_SSL.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "nassl_SSL_CTX.h" 4 | #include "nassl_BIO.h" 5 | 6 | // nassl.SSL Python class 7 | typedef struct { 8 | PyObject_HEAD 9 | SSL *ssl; 10 | nassl_SSL_CTX_Object *sslCtx_Object; 11 | 12 | // We only keep a reference of the network BIO so we know when to free the BIO object 13 | // The internal BIO is auto-freed by SSL_free() which is called in nassl_SSL_dealloc 14 | nassl_BIO_Object *networkBio_Object; 15 | } nassl_SSL_Object; 16 | 17 | 18 | void module_add_SSL(PyObject* m); 19 | 20 | 21 | -------------------------------------------------------------------------------- /nassl/_nassl/python_utils.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | 4 | // Utility function to parse a file path the right way 5 | void *PyArg_ParseFilePath(PyObject *args, char **filePathOut) 6 | { 7 | PyObject *pyFilePath = NULL; 8 | if (!PyArg_ParseTuple(args, "O&", PyUnicode_FSConverter, &pyFilePath)) 9 | { 10 | return NULL; 11 | } 12 | *filePathOut = PyBytes_AsString(pyFilePath); 13 | if (filePathOut == NULL) 14 | { 15 | PyErr_SetString(PyExc_ValueError, "Could not extract the file path"); 16 | return NULL; 17 | } 18 | return filePathOut; 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | bin 3 | .cache 4 | deps 5 | .pytest_cache 6 | .vscode 7 | .ruff_cache/ 8 | .mypy_cache/ 9 | 10 | # OpenSSL build artifacts 11 | *.cnf 12 | *.cnf.dist 13 | 14 | .eggs/* 15 | 16 | # Object files 17 | *.o 18 | 19 | # Python 20 | *.pyc 21 | *.pyd 22 | *.egg-info 23 | lib/* 24 | wheelhouse/* 25 | 26 | # Shared objects (inc. Windows DLLs) 27 | *.dll 28 | *.so 29 | *.so.* 30 | *.dylib 31 | 32 | # Executables 33 | *.exe 34 | *.out 35 | *.app 36 | 37 | # OS X 38 | .DS_Store 39 | 40 | # S3cr3ts 41 | *.key 42 | *.p12 43 | 44 | # Dependencies 45 | openssl*/* 46 | zlib-*/* 47 | 48 | # Build result 49 | MANIFEST 50 | build/* 51 | dist/* 52 | 53 | .idea/* -------------------------------------------------------------------------------- /nassl/_nassl/nassl_X509_STORE_CTX.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // nassl.X509_STORE_CTX Python class 4 | typedef struct { 5 | PyObject_HEAD 6 | X509_STORE_CTX *x509storeCtx; 7 | 8 | // Extra arguments for doing certificate validation; we have to store them here so we can properly free them after 9 | // validation has been completed 10 | STACK_OF(X509) *trustedCertificates; 11 | STACK_OF(X509) *untrustedCertificates; 12 | X509 *leafCertificate; 13 | } nassl_X509_STORE_CTX_Object; 14 | 15 | // Type needs to be accessible to nassl_X509.c 16 | extern PyTypeObject nassl_X509_STORE_CTX_Type; 17 | 18 | void module_add_X509_STORE_CTX(PyObject* m); 19 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Lint & Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Set up Python 3.13 13 | uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.13" 16 | 17 | - name: Install Python dependencies 18 | run: | 19 | python -m pip install --upgrade pip setuptools wheel 20 | pip install -r requirements-dev.txt 21 | 22 | - name: Build C extension 23 | run: invoke build.all 24 | 25 | - name: Lint 26 | run: invoke lint 27 | 28 | - name: Test 29 | run: invoke test -------------------------------------------------------------------------------- /tests/build_config_test.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from sys import platform 3 | from nassl import _nassl, _nassl_legacy 4 | import pytest 5 | 6 | can_only_run_on_linux_64 = pytest.mark.skipif( 7 | condition=platform not in ["linux", "linux2"], 8 | reason="The test suite it not being run on Linux", 9 | ) 10 | 11 | 12 | class TestBuildConfig: 13 | @can_only_run_on_linux_64 14 | @pytest.mark.parametrize("nassl_module", [_nassl, _nassl_legacy]) 15 | def test_internal_openssl_symbols_are_hidden(self, nassl_module): 16 | # Given the compiled _nassl module 17 | # When looking at the module's shared library's symbol table 18 | symbol_table = subprocess.run(["nm", "-gD", f"{nassl_module.__file__}"], capture_output=True).stdout 19 | 20 | # Then internal symbols from the statically linked OpenSSL libraries are not present, so that no 21 | # "symbol confusion" can occur when Python loads the system's OpenSSL libraries (which are incompatible with 22 | # nassl). See also https://github.com/nabla-c0d3/nassl/issues/95 23 | assert "RSA_verify" not in symbol_table.decode("ascii") 24 | -------------------------------------------------------------------------------- /tests/openssl_server/client-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDRzCCAi8CAQEwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UEBhMCQVUxEzARBgNV 3 | BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 4 | ZDEeMBwGA1UEAwwVU1NMeXplIFRlc3QgQ2xpZW50IENBMB4XDTE4MDIyNTAwNTc0 5 | NFoXDTQ2MDcxMzAwNTc0NFowbjELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUt 6 | U3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEnMCUGA1UE 7 | AwweU1NMeXplIFRlc3QgQ2xpZW50IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0B 8 | AQEFAAOCAQ8AMIIBCgKCAQEAycbC+71bV5HfMz6O77+11u4rl14wYs1Nc68tnkTH 9 | ve6z/EB1gAWN/vdaBCJy2+wDvSg2SFjGRhJXu8FgKnY96Yfa4gSZly+xow+cuyQo 10 | gRQY1URLyJEkDFHaH+fUjOYxm+3iw7p84CxjAb5HEFrASbQ9oYBtsl8JN9m2Tx9J 11 | G+/iPZ/xAUo/I/iBSeWS0M/uEf58dB5ZYW3Ce3naABA8OxEFOytR5Cdois5xAd+K 12 | J7SYxtAKly+qDKwiTH4HxqhQH1kzZo/u1JZOATfeW4VkgT6VSvNqaCAnusHdKYBj 13 | eInmisQdwraOyhb1e3ffRUscMx7fhXGZCsK553sH617FswIDAQABMA0GCSqGSIb3 14 | DQEBCwUAA4IBAQB/3cDc8Zvhd6g9N0ylYVWZHO3A448bPzFp7E4IhN+bKobO+XD7 15 | eN8Ibvr6Ry7bsS+9DKqW/nrPnj2LXZpKJJdZ2SEJLLYCkeE1U95Sr7tubMoq7BkZ 16 | THWAjnxo+WB/IsrTAL6mHNnoUv6rb55oNKDqWJFGI6o6HCLPtM2+0S6na/Rp2wlZ 17 | MZM3uj81KQ1IDEn9MghYVHcdxVkvweJ7z8XvO+iyAtrBTFISIYyQ5QaKhC7jR6co 18 | dq9DFN2XIrkC4NDa+2RtmqRBcmKG1MrY/159C+tuCYO5T+6kIkf+HsUYubDcV0G0 19 | G1Q0aYJJ7xP1emM01cEg2fiIp6bPDRIBQNox 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /nassl/ocsp_response.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from nassl import _nassl 4 | 5 | 6 | class OcspResponseNotTrustedError(Exception): 7 | def __init__(self, message: str, trust_store_path: Path) -> None: 8 | super().__init__(message) 9 | self.trust_store_path = trust_store_path 10 | 11 | 12 | def verify_ocsp_response(ocsp_response: _nassl.OCSP_RESPONSE, trust_store_path: Path) -> None: 13 | """Verify that the OCSP response is trusted. 14 | 15 | Args: 16 | ocsp_response: The OCSP response to verify. 17 | trust_store_path: The file path to a trust store containing pem-formatted certificates, to be used for 18 | validating the OCSP response. 19 | 20 | Raises OcspResponseNotTrustedError if the validation failed ie. the OCSP response is not trusted. 21 | """ 22 | # Ensure that the trust store file exists 23 | with trust_store_path.open(): 24 | pass 25 | 26 | try: 27 | ocsp_response.basic_verify(str(trust_store_path)) 28 | except _nassl.OpenSSLError as e: 29 | if "certificate verify error" in str(e): 30 | raise OcspResponseNotTrustedError( 31 | "OCSP Response verification failed: the response is not trusted", 32 | trust_store_path, 33 | ) 34 | raise 35 | -------------------------------------------------------------------------------- /tests/openssl_server/client-ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDnTCCAoWgAwIBAgIJAJ9Z5QwAxWplMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxHjAcBgNVBAMMFVNTTHl6ZSBUZXN0IENsaWVudCBDQTAe 5 | Fw0xODAyMjUwMDU2NDVaFw00NjA3MTMwMDU2NDVaMGUxCzAJBgNVBAYTAkFVMRMw 6 | EQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0 7 | eSBMdGQxHjAcBgNVBAMMFVNTTHl6ZSBUZXN0IENsaWVudCBDQTCCASIwDQYJKoZI 8 | hvcNAQEBBQADggEPADCCAQoCggEBAMoWZoiR7uFFG0bNJC2P+PXJTbDwWQTuV+Gf 9 | NMHwN50BNqGuunLUrpwOXVxT4EHWG2RIlY+0ac+wZaNpigd0gbF5hUNRfuJfVf4Q 10 | 3VN9mW8pzaen7pen4T2RFCOf5x5A3D9vZXbCbgK52NQUH+9bADpOeuxh96SM9TRm 11 | 9dSeEQFCvBGR4v6XqOy6NAcvQKJQpVlWX86l2B7Q6WqsVzvIwwks0+hIGtKdGfHi 12 | Nh3TN+DFHapXxDcbbcXc0rFPAqs9h3P/mQMFe2P4OaT5+J8xjXuRTCRk1S7WFDtg 13 | txxqAUVKTT/kJ587ufyIol3jJG/Xrn7qrI/VYUrEefA0K8rKURcCAwEAAaNQME4w 14 | HQYDVR0OBBYEFPDFbQG4TQwnzwBvs+Lt9oluQxuiMB8GA1UdIwQYMBaAFPDFbQG4 15 | TQwnzwBvs+Lt9oluQxuiMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB 16 | ADFLuJhJDt5Db5KWvB/UTCVzUp78Je8d1mdBDMyro5r7dayCboGtuuUWHhKvQHwN 17 | D+Tuw4ngQsD3YIN9JeEjoxPNiAlrD7EDWZts3yaalcDXUWxo9ree/Vo9sJNO6Ngb 18 | Iu0kogac8tsejSzs5gJFBK16o6zmbHmCSmW+Pcd44UbuzkdLUXLo0d+Y0owAIsbr 19 | 6BECR4yMRRdB/1LrKr2B57P/9GlIiuPYCI7K7qBBXTOM/koUw5odROgmdTjfbAMi 20 | k97inUbnYCu0RHVQiEWBX4US6WxJO4Ly1cBf6BsR1kfBM72Xqyg0QFuweMPTRrAH 21 | J+cCcSM4L3PwgaUztWgGdgc= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /sample_client.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from nassl.ocsp_response import verify_ocsp_response 4 | from nassl.ssl_client import OpenSslVersionEnum, SslClient, OpenSslVerifyEnum 5 | import socket 6 | 7 | mozilla_store = Path("tests") / "mozilla.pem" 8 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 9 | sock.settimeout(5) 10 | 11 | hostname = "www.cloudflare.com" 12 | sock.connect((hostname, 443)) 13 | 14 | ssl_client = SslClient( 15 | ssl_version=OpenSslVersionEnum.TLSV1_2, 16 | underlying_socket=sock, 17 | ssl_verify=OpenSslVerifyEnum.PEER, 18 | ssl_verify_locations=mozilla_store, 19 | ) 20 | ssl_client.set_tlsext_status_ocsp() 21 | ssl_client.do_handshake() 22 | 23 | print("Received certificate chain") 24 | for pem_cert in ssl_client.get_received_chain(): 25 | print(pem_cert) 26 | 27 | print("Verified certificate chain") 28 | for pem_cert in ssl_client.get_verified_chain(): 29 | print(pem_cert) 30 | 31 | ocsp_resp = ssl_client.get_tlsext_status_ocsp_resp() 32 | if ocsp_resp: 33 | print("OCSP Stapling") 34 | verify_ocsp_response(ocsp_resp, Path(mozilla_store)) 35 | 36 | print("\nCipher suite") 37 | print(ssl_client.get_current_cipher_name()) 38 | 39 | print("\nEphemeral Key") 40 | print(ssl_client.get_ephemeral_key()) 41 | 42 | print("\nHTTP response") 43 | ssl_client.write(f"GET / HTTP/1.0\r\nUser-Agent: Test\r\nHost: {hostname}\r\n\r\n".encode("ascii")) 44 | print(ssl_client.read(2048)) 45 | -------------------------------------------------------------------------------- /nassl/_nassl/openssl_utils.c: -------------------------------------------------------------------------------- 1 | 2 | #include "openssl_utils.h" 3 | 4 | 5 | PyObject* generic_print_to_string(int (*openSslPrintFunction)(BIO *fp, const void *a), const void *dataStruct) 6 | { 7 | BIO *memBio; 8 | char *dataTxtBuffer; 9 | int dataTxtSize; 10 | PyObject* res; 11 | 12 | memBio = BIO_new(BIO_s_mem()); 13 | if (memBio == NULL) 14 | { 15 | raise_OpenSSL_error(); 16 | return NULL; 17 | } 18 | 19 | openSslPrintFunction(memBio, dataStruct); 20 | dataTxtSize = BIO_pending(memBio); 21 | 22 | dataTxtBuffer = (char *) PyMem_Malloc(dataTxtSize); 23 | if (dataTxtBuffer == NULL) 24 | { 25 | BIO_vfree(memBio); 26 | return PyErr_NoMemory(); 27 | } 28 | 29 | // Extract the text from the BIO 30 | BIO_read(memBio, dataTxtBuffer, dataTxtSize); 31 | res = PyUnicode_FromStringAndSize(dataTxtBuffer, dataTxtSize); 32 | PyMem_Free(dataTxtBuffer); 33 | BIO_vfree(memBio); 34 | return res; 35 | } 36 | 37 | 38 | PyObject *bioToPyString(BIO *memBio) 39 | { 40 | char *dataTxtBuffer; 41 | unsigned int dataTxtSize; 42 | PyObject* res; 43 | 44 | dataTxtSize = BIO_pending(memBio); 45 | dataTxtBuffer = (char *) PyMem_Malloc(dataTxtSize); 46 | if (dataTxtBuffer == NULL) 47 | { 48 | return PyErr_NoMemory(); 49 | } 50 | 51 | // Extract the text from the BIO 52 | BIO_read(memBio, dataTxtBuffer, dataTxtSize); 53 | res = PyUnicode_FromStringAndSize(dataTxtBuffer, dataTxtSize); 54 | PyMem_Free(dataTxtBuffer); 55 | return res; 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels_linux_aarch64.yml: -------------------------------------------------------------------------------- 1 | # Workflow just for linux aarch64 wheels so it can run in parallel with the build_wheels workflow 2 | name: Build aarch64 Wheels 3 | 4 | on: 5 | push: 6 | branches: [ release ] 7 | pull_request: 8 | branches: [ release ] 9 | 10 | jobs: 11 | build_wheels: 12 | name: Build aarch64 wheels on ${{ matrix.os }} 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-22.04] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: actions/setup-python@v5 22 | name: Install Python 23 | with: 24 | python-version: '3.10' 25 | 26 | - name: Install cibuildwheel 27 | run: python -m pip install "cibuildwheel>=2.22,<2.23" 28 | 29 | # Needed for Linux aarch64 builds 30 | - name: Set up QEMU 31 | if: runner.os == 'Linux' 32 | uses: docker/setup-qemu-action@v1 33 | with: 34 | platforms: all 35 | 36 | - name: Build wheels 37 | run: python -m cibuildwheel --output-dir wheelhouse 38 | env: 39 | CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*" 40 | CIBW_ARCHS_LINUX: aarch64 # Specifically build linux aarch64 wheels 41 | CIBW_MANYLINUX_AARCH64_IMAGE: manylinux2014 42 | CIBW_BEFORE_ALL: "python -m pip install setuptools invoke && invoke build.deps" 43 | CIBW_BEFORE_BUILD: "python -m pip install setuptools invoke && invoke build.nassl" 44 | CIBW_TEST_REQUIRES: "pytest" 45 | CIBW_TEST_COMMAND: "python -m pytest {project}/tests" 46 | 47 | - uses: actions/upload-artifact@v4 48 | with: 49 | name: ${{ matrix.os }}-wheels 50 | path: ./wheelhouse/*.whl 51 | -------------------------------------------------------------------------------- /tests/openssl_server/client-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJxsL7vVtXkd8z 3 | Po7vv7XW7iuXXjBizU1zry2eRMe97rP8QHWABY3+91oEInLb7AO9KDZIWMZGEle7 4 | wWAqdj3ph9riBJmXL7GjD5y7JCiBFBjVREvIkSQMUdof59SM5jGb7eLDunzgLGMB 5 | vkcQWsBJtD2hgG2yXwk32bZPH0kb7+I9n/EBSj8j+IFJ5ZLQz+4R/nx0HllhbcJ7 6 | edoAEDw7EQU7K1HkJ2iKznEB34ontJjG0AqXL6oMrCJMfgfGqFAfWTNmj+7Ulk4B 7 | N95bhWSBPpVK82poICe6wd0pgGN4ieaKxB3Cto7KFvV7d99FSxwzHt+FcZkKwrnn 8 | ewfrXsWzAgMBAAECggEAU+5+2vJ4VWPTQWCrWmUXgbEOpudCH0chCZb71dLsd0Ac 9 | 1DgH6FnnKADCC+g8eOii4YMhmVR8HVex8OLOWrtWo5akYNHjBbWMIbTz0BCJXK/8 10 | aHIBSAu/v/QoVI89peJ3DlKUujAPk7xC7s20h8QIKmB0JoLinojTr8y6/gyYf6q5 11 | vAl7gZol3Dagh9UXNy2R0ezicfDvtvYKI1wD7pb+Mw5bOCwSlRf1oT9pPDMP1Ose 12 | fv/tQy8u4bF2HIbRrDWHAdomRsueFTb0dvwoO/DiHNQUPeEAnkEncSzxJXayxwPl 13 | 8P9qREDMVSH+5MjsgHPK1aEyrV+9Phke41lb9GTc0QKBgQD/1wqWB3yb/MrGB/96 14 | vSq6VixuTD8ykJdxtkolQPmBTqlbvlx7AMTEu8kueFCzrshif+8mycJOaAdLCyES 15 | 9wBASlZV602P5tNhXgwy6pw3GDu/nRSpvSBuar8JDyquacHNstJMmwYiCg03duuH 16 | WX2tA5VeaLtu2oDV2MJCy0BRewKBgQDJ5xCkDa/HqSQzyszQVrAFK39gRQuGo7Uv 17 | JdWD1X7nOnSLJCXH5itUyxhH6/94N9FBvebU6smomeNptn54R9/ZmOGLtS6fkQmJ 18 | PMJVBHmr8XmZKA9Hc0k359FMhoRlRIJHDmEQ8v9PhH0PUxWazFTa39QZ6Y526X65 19 | VbQqbOFbKQKBgQC6wl7u8F4tfJcFgtcj4S1swvVCOxSzM8vp7Xkowsqgcyy8VTUr 20 | cX8yYibVbmzzDfcnuF57ATN/iv8v79rf/kFHrTxjEhcXohfSbxYWoR8SNPWAxglM 21 | c0xWbkQwN8sfcQJRx2UvGMecV5wYTg5XSqOshf4m4etZW9ZKxSXiHn9AOwKBgQCH 22 | IKSch6R7xpI6L6LAVSRdcW1AqzU5mVtsALBUGZFjhFX3weufTEb36y2HBUXn2cOt 23 | ckGJgtIQi14OpFskeUYyRgW+ETbxCIsPVKDjcalVELpHbO487cW1KuuDggweEqn2 24 | kIMaaufA+nUQypSNE/A+xMsZxJzarQ9pTxjTxCdXcQKBgBPgLIcF9Pq11B1PiWET 25 | lqyfEjgr/upOYneaJfE050AiXSxQIedNfZk1l2w1jJ2BjFGs1nwSuTxRhsOrX3gd 26 | btI4sDL7Q0gYYL0Rn5zFKmoN85oX4/Uw1P137vFA8RrI1GEZ3Ykc62jgz0p7tLHz 27 | eADSpY3caQE1jk2wNXpp6IFW 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/openssl_server/server-self-signed-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFizCCA3OgAwIBAgIJAMq7WZA0Vg7fMA0GCSqGSIb3DQEBCwUAMFwxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxFTATBgNVBAMMDHNzbHl6ZS10ZXN0czAeFw0xNzAzMDUy 5 | MDMxNTRaFw0xODAzMDUyMDMxNTRaMFwxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApT 6 | b21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxFTAT 7 | BgNVBAMMDHNzbHl6ZS10ZXN0czCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC 8 | ggIBAJ0VkRA2IVVzSeAXMzSSS7iI5WvAU+zWA/bml4o0QWYoKjEuZESOgL0mU8x9 9 | OV47eGG4CMTnWOkokaCKmWGzXqvhs/0RLP3/HYuWIXrD1CsQwELpjAp5vtdRyyjv 10 | YPaWljLOGduofY72d2TF07PFE3YsvHstGDe8/ADnsNwXv+hCGF+9fk0woTl0lH7k 11 | QCQn9h5Q3qGZYKAtMv/bdPYnBgIf26/p4BLfv6dwfYHV95f3YnrAqCYma1oQZtn+ 12 | tOgAQcbDVdiQNYWLqwGe/JRdwXBSn5aEu41B+To9auxWMe2J+uJpl+KCvoKflZhj 13 | +Ov/OwyfBNyxP0SdTn12CCdYwunJFoQ1Cu8/N9puo2Nm4LuXQKmS8QZN0Shj5h1g 14 | IjaMXJ/do7R5FBIejQH3fxdoOmoKHKrdj+k4OczrDRH0ieH9pC9pPab8VgtVlsxb 15 | n8b+KFd9k/cFRnQsqPcPyFfFdH19qOQfJGK+uDFpqoJMiSzDhTndwoJ36rZ4LppH 16 | 4oNFS5Qj+5FD3f0Q7C3cwkTvZZ6AXAL+wd4tYUZ6gAMFjNa/Wt8xm3baPL96SsKL 17 | TyKX5pkokKk3VxHwVWw+9Q2Nicl3GYynm2K06YcLugZa5mrO3vHeV1DO9WThtgKd 18 | xkGmDWdua6PHPqtIY8prXENkBm211wD+9cFwBt7YBJ1Z+lVPAgMBAAGjUDBOMB0G 19 | A1UdDgQWBBQ8s8ALD5cw1DPNE7aHNKmT0MK7aTAfBgNVHSMEGDAWgBQ8s8ALD5cw 20 | 1DPNE7aHNKmT0MK7aTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAV 21 | RR+Oey/GhwRJPbiXT7dOLWrwXgizm3UoE8SQsMJIjMw49WygdEVRJa3EmG2aLkNX 22 | atyKqcWzuoIo0OK0rTAM1TWWrN7nBo8+EzfTg7orKj3+HOLLLXsW2Kf8Pi8+0QLP 23 | +9moUwx4TNd9XQdD7c8AR9vSPn3eoZj/qMBkSJV35vJhVYYUAiInliVdr+PobLV7 24 | +WtYs/NweT3voucTX2QmtwEDs4FMVuFOW6rtF+MOapvg+EnRB6Wvw0BBhJ4u19d0 25 | 4vERfKhBNzxN+5kaKfp09EMKJN4W0TGJWC6CRpn4yuYOZqnuUqZHpfwNoxulpo/V 26 | FlUexMTyzwePk/B6lX7rCcRLSpKpJMd9ba23X+3jFgdLGESa1/NVINaeSFZoXEa9 27 | lJiuEXLdR6FbRVXnbsUd6y2aqBOfRiA7rO0RLjaHqPNdAgcoBFd7LPfUfuQGOoHk 28 | I9B3nG+AfjU+UnfKT+cWy+9KfAOl60sjzkxfX8KOJ5TCJ1CAxpjGmfv3DFJxm7aF 29 | 9QhT/oqxe+NetK4odTolL/3cSo5/FokfY+N0mKrERARisDaneu+bmtVMVJzBDUeT 30 | sI12gU6L3DdvM8r888ok+eCNAzMpNRWZfgK8cfCfoiH6ki+vkNz58F+DN/UJZPCo 31 | XMgGUwLZNTDnAbGucjr9t2mG9JPrnNTj39zBvGtITg== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /tests/X509_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nassl import _nassl 4 | from nassl import _nassl_legacy 5 | 6 | 7 | @pytest.mark.parametrize("nassl_module", [_nassl, _nassl_legacy]) 8 | class TestX509: 9 | def test_from_pem(self, nassl_module): 10 | # Given a PEM-formatted certificate 11 | pem_cert = """-----BEGIN CERTIFICATE----- 12 | MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG 13 | A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv 14 | b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw 15 | MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i 16 | YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT 17 | aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ 18 | jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp 19 | xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp 20 | 1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG 21 | snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ 22 | U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 23 | 9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E 24 | BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B 25 | AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz 26 | yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE 27 | 38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP 28 | AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad 29 | DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME 30 | HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== 31 | -----END CERTIFICATE----- 32 | """ 33 | 34 | # When parsing it 35 | certificate = nassl_module.X509(pem_cert) 36 | 37 | # It succeeds 38 | assert certificate 39 | assert certificate.as_text() 40 | assert pem_cert == certificate.as_pem() 41 | 42 | def test_from_pem_bad(self, nassl_module): 43 | pem_cert = "123123" 44 | with pytest.raises(ValueError): 45 | nassl_module.X509(pem_cert) 46 | 47 | def test_verify_cert_error_string(self, nassl_module): 48 | assert nassl_module.X509.verify_cert_error_string(1) 49 | -------------------------------------------------------------------------------- /tests/X509_STORE_CTX_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nassl._nassl import X509, X509_STORE_CTX 4 | 5 | 6 | @pytest.fixture 7 | def certificate_as_x509() -> X509: 8 | pem_cert = """-----BEGIN CERTIFICATE----- 9 | MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG 10 | A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv 11 | b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw 12 | MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i 13 | YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT 14 | aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ 15 | jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp 16 | xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp 17 | 1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG 18 | snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ 19 | U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 20 | 9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E 21 | BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B 22 | AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz 23 | yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE 24 | 38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP 25 | AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad 26 | DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME 27 | HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== 28 | -----END CERTIFICATE-----""" 29 | return X509(pem_cert) 30 | 31 | 32 | class TestX509_STORE_CTX: 33 | def test_set0_trusted_stack(self, certificate_as_x509): 34 | ctx = X509_STORE_CTX() 35 | ctx.set0_trusted_stack([certificate_as_x509, certificate_as_x509]) 36 | 37 | # When calling it a second time it fails 38 | with pytest.raises(ValueError): 39 | ctx.set0_trusted_stack([certificate_as_x509, certificate_as_x509]) 40 | 41 | def test_set0_untrusted(self, certificate_as_x509): 42 | ctx = X509_STORE_CTX() 43 | ctx.set0_untrusted([certificate_as_x509, certificate_as_x509]) 44 | 45 | # When calling it a second time it fails 46 | with pytest.raises(ValueError): 47 | ctx.set0_untrusted([certificate_as_x509, certificate_as_x509]) 48 | 49 | def test_set_cert(self, certificate_as_x509): 50 | ctx = X509_STORE_CTX() 51 | ctx.set_cert(certificate_as_x509) 52 | 53 | # When calling it a second time it fails 54 | with pytest.raises(ValueError): 55 | ctx.set_cert(certificate_as_x509) 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nassl 2 | ===== 3 | 4 | ![Build Wheels](https://github.com/nabla-c0d3/nassl/workflows/Build%20Wheels/badge.svg) 5 | [![PyPI version](https://img.shields.io/pypi/v/nassl.svg)](https://pypi.org/project/nassl/) 6 | [![PyPI wheel](https://img.shields.io/pypi/wheel/nassl.svg)](https://pypi.org/project/nassl/) 7 | [![PyPI version](https://img.shields.io/pypi/pyversions/nassl.svg)](https://pypi.org/project/nassl/) 8 | 9 | Experimental OpenSSL wrapper for Python 3.9+ and [SSLyze](https://github.com/nabla-c0d3/sslyze). 10 | 11 | **Do NOT use for anything serious**. This code has not been properly tested/reviewed and is not production ready. 12 | 13 | 14 | Quick Start 15 | ----------- 16 | 17 | Nassl can be installed directly via pip: 18 | 19 | pip install nassl 20 | 21 | Development environment 22 | ----------------------- 23 | 24 | To setup a development environment: 25 | 26 | $ pip install --upgrade pip setuptools wheel 27 | $ pip install -r requirements-dev.txt 28 | 29 | Nassl relies on a C extension to call into OpenSSL; you can compile everything using: 30 | 31 | $ invoke build.all 32 | 33 | Then, the tests can be run using: 34 | 35 | $ invoke test 36 | 37 | 38 | Project structure 39 | ----------------- 40 | 41 | ### nassl/ 42 | 43 | Classes implemented in Python are part of the `nassl` namespace; they are designed to provide a simpler, higher-level 44 | interface to perform SSL connections. 45 | 46 | 47 | ### nassl/_nassl/ 48 | 49 | Classes implemented in C are part of the `nassl._nassl` namespace; they try to stay as close as possible to OpenSSL's 50 | API. In most cases, Python methods of such objects directly match the OpenSSL function with same name. For example the 51 | `_nassl.SSL.read()` Python method matches OpenSSL's `SSL_read()` function. 52 | 53 | These classes should be considered internal. 54 | 55 | 56 | Why another SSL library? 57 | ------------------------ 58 | 59 | I'm the author of [SSLyze](https://github.com/nabla-c0d3/sslyze), an SSL scanner written in Python. Scanning SSL servers 60 | requires access to low-level SSL functions within the OpenSSL API, for example to test for things like insecure 61 | renegotiation or session resumption. 62 | 63 | None of the existing OpenSSL wrappers for Python (including ssl, M2Crypto and pyOpenSSL) expose the APIs that I need for 64 | SSLyze, so I had to write my own wrapper. 65 | 66 | 67 | License 68 | ------- 69 | 70 | See ./LICENSE.txt 71 | 72 | Please contact me if this license doesn't work for you. 73 | 74 | 75 | Author 76 | ------ 77 | 78 | Alban Diquet - @nabla_c0d3 - https://nabla-c0d3.github.io 79 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from invoke import task, Collection 4 | 5 | import build_tasks 6 | from nassl import __version__ 7 | 8 | root_path = Path(__file__).parent.absolute() 9 | 10 | 11 | @task 12 | def test(ctx): 13 | ctx.run("pytest --durations 5") 14 | ctx.run("python sample_client.py") 15 | 16 | 17 | @task 18 | def lint(ctx): 19 | ctx.run("ruff format .") 20 | ctx.run("ruff check . --fix") 21 | ctx.run("mypy sample_client.py nassl") 22 | 23 | 24 | @task 25 | def package_linux_wheels(ctx): 26 | """Build the Linux 32 and 64 bit wheels using Docker.""" 27 | ctx.run(f"docker run --rm -v {root_path}:/io quay.io/pypa/manylinux2010_i686 bash /io/build_linux_wheels.sh") 28 | ctx.run(f"docker run --rm -v {root_path}:/io quay.io/pypa/manylinux2010_x86_64 bash /io/build_linux_wheels.sh") 29 | 30 | 31 | @task 32 | def package_wheel(ctx): 33 | """Build the binary wheel for the current system; works on Windows anc macOS.""" 34 | ctx.run("python setup.py bdist_wheel") 35 | 36 | 37 | @task 38 | def package_windows_wheels(ctx): 39 | """Build the binary wheels for Windows; this expects Python to be installed at specific locations.""" 40 | for python_exe in [ 41 | "%userprofile%\\AppData\\Local\\Programs\\Python\\Python37\\python.exe", 42 | "%userprofile%\\AppData\\Local\\Programs\\Python\\Python38\\python.exe", 43 | ]: 44 | ctx.run(f"{python_exe} setup.py bdist_wheel") 45 | 46 | 47 | @task 48 | def release(ctx): 49 | raise NotImplementedError() 50 | response = input(f'Release version "{__version__}" ? y/n') 51 | if response.lower() != "y": 52 | print("Cancelled") 53 | return 54 | 55 | # Ensure the tests pass 56 | test(ctx) 57 | 58 | # Add the git tag 59 | ctx.run(f"git tag -a {__version__} -m '{__version__}'") 60 | ctx.run("git push --tags") 61 | 62 | # Build the Windows wheel 63 | package_wheel(ctx) 64 | 65 | # Build the Linux wheels 66 | package_linux_wheels(ctx) 67 | 68 | 69 | # Setup all the tasks 70 | ns = Collection() 71 | ns.add_task(release) 72 | ns.add_task(test) 73 | ns.add_task(lint) 74 | 75 | 76 | package = Collection("package") 77 | package.add_task(package_linux_wheels, "linux_wheels") 78 | package.add_task(package_windows_wheels, "windows_wheels") 79 | package.add_task(package_wheel, "wheel") 80 | ns.add_collection(package) 81 | 82 | build = Collection("build") 83 | build.add_task(build_tasks.build_zlib, "zlib") 84 | build.add_task(build_tasks.build_legacy_openssl, "legacy_openssl") 85 | build.add_task(build_tasks.build_modern_openssl, "modern_openssl") 86 | build.add_task(build_tasks.build_deps, "deps") 87 | build.add_task(build_tasks.build_nassl, "nassl") 88 | build.add_task(build_tasks.build_all, "all") 89 | ns.add_collection(build) 90 | -------------------------------------------------------------------------------- /tests/ocsp_response_test.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | 5 | import socket 6 | import tempfile 7 | 8 | from nassl.legacy_ssl_client import LegacySslClient 9 | from nassl.ocsp_response import OcspResponseNotTrustedError, verify_ocsp_response 10 | from nassl.ssl_client import SslClient, OpenSslVerifyEnum 11 | 12 | 13 | _CERTIFICATE_AS_PEM = """-----BEGIN CERTIFICATE----- 14 | MIIDCjCCAnOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBgDELMAkGA1UEBhMCRlIx 15 | DjAMBgNVBAgMBVBhcmlzMQ4wDAYDVQQHDAVQYXJpczEWMBQGA1UECgwNRGFzdGFy 16 | ZGx5IEluYzEMMAoGA1UECwwDMTIzMQ8wDQYDVQQDDAZBbCBCYW4xGjAYBgkqhkiG 17 | 9w0BCQEWC2xvbEBsb2wuY29tMB4XDTEzMDEyNzAwMDM1OFoXDTE0MDEyNzAwMDM1 18 | OFowgZcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDAMxMjMxDTALBgNVBAcMBFRlc3Qx 19 | IjAgBgNVBAoMGUludHJvc3B5IFRlc3QgQ2xpZW50IENlcnQxCzAJBgNVBAsMAjEy 20 | MRUwEwYDVQQDDAxBbGJhbiBEaXF1ZXQxIzAhBgkqhkiG9w0BCQEWFG5hYmxhLWMw 21 | ZDNAZ21haWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlnvP1ltVO 22 | 8JDNT3AA99QqtiqCi/7BeEcFDm2al46mv7looz6CmB84osrusNVFsS5ICLbrCmeo 23 | w5sxW7VVveGueBQyWynngl2PmmufA5Mhwq0ZY8CvwV+O7m0hEXxzwbyGa23ai16O 24 | zIiaNlBAb0mC2vwJbsc3MTMovE6dHUgmzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG 25 | CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV 26 | HQ4EFgQUYR45okpFsqTYB1wlQQblLH9cRdgwHwYDVR0jBBgwFoAUP0X2HQlaca7D 27 | NBzVbsjsdhzOqUQwDQYJKoZIhvcNAQEFBQADgYEAWEOxpRjvKvTurDXK/sEUw2KY 28 | gmbbGP3tF+fQ/6JS1VdCdtLxxJAHHTW62ugVTlmJZtpsEGlg49BXAEMblLY/K7nm 29 | dWN8oZL+754GaBlJ+wK6/Nz4YcuByJAnN8OeTY4Acxjhks8PrAbZgcf0FdpJaAlk 30 | Pd2eQ9+DkopOz3UGU7c= 31 | -----END CERTIFICATE-----""" 32 | 33 | 34 | @pytest.mark.parametrize("ssl_client_cls", [SslClient, LegacySslClient]) 35 | class TestCommonOcspResponseOnline: 36 | def test(self, ssl_client_cls): 37 | # Given a website that support OCSP stapling 38 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 39 | sock.settimeout(5) 40 | sock.connect(("www.apple.com", 443)) 41 | 42 | ssl_client = ssl_client_cls(underlying_socket=sock, ssl_verify=OpenSslVerifyEnum.NONE) 43 | ssl_client.set_tlsext_status_ocsp() 44 | ssl_client.do_handshake() 45 | 46 | # When retrieving the stapled OCSP response, it succeeds 47 | ocsp_response = ssl_client.get_tlsext_status_ocsp_resp() 48 | ssl_client.shutdown() 49 | 50 | # And the OCSP response is valid 51 | assert ocsp_response.as_text() 52 | assert ocsp_response.as_der_bytes() 53 | 54 | # And given a wrong certificate 55 | with tempfile.NamedTemporaryFile(delete=False, mode="wt") as test_file: 56 | test_file.write(_CERTIFICATE_AS_PEM) 57 | test_file.close() 58 | # Trying to verify fails with the right error 59 | with pytest.raises(OcspResponseNotTrustedError): 60 | verify_ocsp_response(ocsp_response, Path(test_file.name)) 61 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels.yml: -------------------------------------------------------------------------------- 1 | name: Build Wheels 2 | 3 | on: 4 | push: 5 | branches: [ release ] 6 | pull_request: 7 | branches: [ release ] 8 | 9 | jobs: 10 | build_wheels: 11 | name: Build wheels on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-22.04, macos-14, windows-2022] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install Windows 8.1 SDK 21 | shell: powershell 22 | run: | 23 | Invoke-WebRequest -Method Get -Uri https://go.microsoft.com/fwlink/p/?LinkId=323507 -OutFile sdksetup.exe -UseBasicParsing 24 | Start-Process -Wait sdksetup.exe -ArgumentList "/q", "/norestart", "/features", "OptionId.WindowsDesktopSoftwareDevelopmentKit", "OptionId.NetFxSoftwareDevelopmentKit" 25 | if: runner.os == 'Windows' 26 | 27 | - name: Install VS 2015 tools for Zlib 28 | shell: powershell 29 | run: | 30 | Invoke-WebRequest -Method Get -Uri https://aka.ms/vs/17/release/vs_enterprise.exe -OutFile vs_enterprise.exe 31 | Start-Process -Wait vs_enterprise.exe -ArgumentList 'modify', '--installPath "C:\Program Files\Microsoft Visual Studio\2022\Enterprise"', '--add', 'Microsoft.VisualStudio.Component.VC.140', '--quiet', '--norestart', '--wait' 32 | if: runner.os == 'Windows' 33 | 34 | - uses: actions/setup-python@v5 35 | name: Install Python 36 | with: 37 | python-version: '3.10' 38 | 39 | - name: Install cibuildwheel 40 | run: python -m pip install "cibuildwheel>=2.22,<2.23" 41 | 42 | - name: Build wheels 43 | run: python -m cibuildwheel --output-dir wheelhouse 44 | env: 45 | CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-* cp313-*" 46 | CIBW_SKIP: "*-win32 pp* *-musllinux_i686" # Skip win32, PyPy and muslinux32 builds 47 | CIBW_ARCHS_MACOS: "native" # macos-14 is apple silicon ie. arm64 48 | # Build manylinux2014 wheels 49 | CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 50 | CIBW_MANYLINUX_I686_IMAGE: manylinux2014 51 | # The C libraries (OpenSSL and Zlib) only need to be built once per OS 52 | # as they are not tied to a specific Python version 53 | CIBW_BEFORE_ALL: "python -m pip install setuptools invoke && invoke build.deps" 54 | CIBW_BEFORE_ALL_WINDOWS: '"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && python -m pip install invoke && invoke build.deps' 55 | # However the nassl C extension is by design tied to a specific Python version 56 | CIBW_BEFORE_BUILD: "python -m pip install setuptools invoke && invoke build.nassl" 57 | CIBW_TEST_REQUIRES: "pytest" 58 | CIBW_TEST_COMMAND: "python -m pytest {project}/tests" 59 | 60 | - uses: actions/upload-artifact@v4 61 | with: 62 | name: ${{ matrix.os }}-wheels 63 | path: ./wheelhouse/*.whl 64 | -------------------------------------------------------------------------------- /tests/ephemeral_key_info_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nassl.ephemeral_key_info import ( 4 | EcDhEphemeralKeyInfo, 5 | OpenSslEcNidEnum, 6 | OpenSslEvpPkeyEnum, 7 | _OPENSSL_NID_TO_SECG_ANSI_X9_62, 8 | _OPENSSL_EVP_PKEY_TO_NAME_MAPPING, 9 | DhEphemeralKeyInfo, 10 | ) 11 | from nassl.ssl_client import SslClient 12 | 13 | 14 | class TestOpenSslEcNidEnum: 15 | def test_supported_by_ssl_client(self): 16 | # Ensure the expected NIDs can be used to configure an SslClient 17 | for ec_nid in OpenSslEcNidEnum.get_supported_by_ssl_client(): 18 | ssl_client = SslClient() 19 | ssl_client.set_groups([ec_nid]) 20 | 21 | @pytest.mark.skip("TODO: Fix brainpool support; see also https://github.com/nabla-c0d3/nassl/issues/104") 22 | def test_brainpool_fix_me(self): 23 | # Brainpool NIDs will trigger an OpenSslError 24 | ssl_client = SslClient() 25 | ssl_client.set_groups([OpenSslEcNidEnum.brainpoolP160r1]) 26 | 27 | 28 | class TestEphemeralKeyInfo: 29 | def test_evp_pkey_to_name_mapping(self): 30 | # Ensure all known EVP PKEYs have an associated name 31 | for evp_pkey in OpenSslEvpPkeyEnum: 32 | assert evp_pkey in _OPENSSL_EVP_PKEY_TO_NAME_MAPPING 33 | 34 | def test_ec_nid_to_name_mapping(self): 35 | # Ensure all known NIDs have an associated name 36 | for ec_nid in OpenSslEcNidEnum: 37 | assert ec_nid in _OPENSSL_NID_TO_SECG_ANSI_X9_62 38 | 39 | def test_ec_dh(self): 40 | # Given some key info returned by OpenSSL 41 | openssl_key_info = dict( 42 | type=OpenSslEvpPkeyEnum.EC, 43 | size=12, 44 | public_bytes=bytearray(b"123"), 45 | curve=OpenSslEcNidEnum.X448, 46 | ) 47 | 48 | # When parsing it, it succeeds 49 | key_info = EcDhEphemeralKeyInfo(**openssl_key_info) 50 | assert key_info 51 | 52 | def test_ec_dh_unknown_curve(self): 53 | # Given some key info returned by OpenSSL with an unknown curve ID 54 | openssl_key_info = dict( 55 | curve=12345, 56 | type=OpenSslEvpPkeyEnum.EC, 57 | size=12, 58 | public_bytes=bytearray(b"123"), 59 | ) 60 | 61 | # When parsing it, it succeeds 62 | key_info = EcDhEphemeralKeyInfo(**openssl_key_info) 63 | assert key_info 64 | assert "unknown" in key_info.curve_name 65 | 66 | def test_dh_unknown_type(self): 67 | # Given some key info returned by OpenSSL with an unknown type 68 | openssl_key_info = dict( 69 | type=12345, 70 | size=12, 71 | public_bytes=bytearray(b"123"), 72 | prime=bytearray(b"123"), 73 | generator=bytearray(b"123"), 74 | ) 75 | 76 | # When parsing it, it succeeds 77 | key_info = DhEphemeralKeyInfo(**openssl_key_info) 78 | assert key_info 79 | assert "UNKNOWN" in key_info.type_name 80 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // Fix symbol clashing on Windows 4 | // https://bugs.launchpad.net/pyopenssl/+bug/570101 5 | #ifdef _WIN32 6 | #include "winsock.h" 7 | #endif 8 | 9 | #include 10 | #include 11 | 12 | #include "nassl_errors.h" 13 | #include "nassl_SSL_CTX.h" 14 | #include "nassl_SSL.h" 15 | #include "nassl_BIO.h" 16 | #include "nassl_X509.h" 17 | #include "nassl_SSL_SESSION.h" 18 | #include "nassl_OCSP_RESPONSE.h" 19 | 20 | #ifndef LEGACY_OPENSSL 21 | #include "nassl_X509_STORE_CTX.h" 22 | #endif 23 | 24 | 25 | static PyMethodDef nassl_methods[] = 26 | { 27 | {NULL} /* Sentinel */ 28 | }; 29 | 30 | struct module_state 31 | { 32 | PyObject *error; 33 | }; 34 | 35 | #define GETSTATE(m) ((struct module_state*)PyModule_GetState(m)) 36 | 37 | 38 | static int nassl_traverse(PyObject *m, visitproc visit, void *arg) 39 | { 40 | Py_VISIT(GETSTATE(m)->error); 41 | return 0; 42 | } 43 | 44 | static int nassl_clear(PyObject *m) 45 | { 46 | Py_CLEAR(GETSTATE(m)->error); 47 | return 0; 48 | } 49 | 50 | 51 | static struct PyModuleDef moduledef = 52 | { 53 | PyModuleDef_HEAD_INIT, 54 | 55 | #ifdef LEGACY_OPENSSL 56 | "_nassl_legacy", 57 | #else 58 | "_nassl", 59 | #endif 60 | 61 | NULL, 62 | sizeof(struct module_state), 63 | nassl_methods, 64 | NULL, 65 | nassl_traverse, 66 | nassl_clear, 67 | NULL 68 | }; 69 | 70 | #define INITERROR return NULL 71 | 72 | 73 | #ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ 74 | #define PyMODINIT_FUNC void 75 | #endif 76 | 77 | 78 | #ifdef LEGACY_OPENSSL 79 | PyMODINIT_FUNC PyInit__nassl_legacy(void) 80 | #else 81 | PyMODINIT_FUNC PyInit__nassl(void) 82 | #endif 83 | 84 | { 85 | PyObject* module; 86 | struct module_state *state; 87 | 88 | // Initialize OpenSSL 89 | #ifdef LEGACY_OPENSSL 90 | SSL_library_init(); 91 | SSL_load_error_strings(); 92 | #else 93 | OPENSSL_init_ssl(0, NULL); 94 | #endif 95 | 96 | // Check OpenSSL PRNG 97 | if(RAND_status() != 1) { 98 | PyErr_SetString(PyExc_EnvironmentError, "OpenSSL PRNG not seeded with enough data"); 99 | INITERROR; 100 | } 101 | 102 | // Initialize the module 103 | module = PyModule_Create(&moduledef); 104 | if (module == NULL) 105 | { 106 | INITERROR; 107 | } 108 | 109 | if (!module_add_errors(module)) 110 | { 111 | INITERROR; 112 | } 113 | module_add_SSL_CTX(module); 114 | module_add_SSL(module); 115 | module_add_BIO(module); 116 | module_add_X509(module); 117 | module_add_SSL_SESSION(module); 118 | module_add_OCSP_RESPONSE(module); 119 | 120 | 121 | #ifndef LEGACY_OPENSSL 122 | // Only available in modern nassl 123 | module_add_X509_STORE_CTX(module); 124 | #endif 125 | 126 | state = GETSTATE(module); 127 | state->error = PyErr_NewException("nassl._nassl.Error", NULL, NULL); 128 | if (state->error == NULL) 129 | { 130 | Py_DECREF(module); 131 | INITERROR; 132 | } 133 | 134 | return module; 135 | } 136 | -------------------------------------------------------------------------------- /nassl/cert_chain_verifier.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import List 3 | from nassl._nassl import X509, X509_STORE_CTX 4 | 5 | 6 | class CertificateChainVerificationFailed(Exception): 7 | def __init__(self, openssl_error_code: int) -> None: 8 | self.openssl_error_code = openssl_error_code 9 | self.openssl_error_string = X509.verify_cert_error_string(self.openssl_error_code) 10 | super().__init__( 11 | f'Verification failed with OpenSSL error code {self.openssl_error_code}: "{self.openssl_error_string}"' 12 | ) 13 | 14 | 15 | class CertificateChainVerifier: 16 | def __init__(self, trusted_certificates: List[X509]) -> None: 17 | if not trusted_certificates: 18 | raise ValueError("Supplied an empty list of trusted certificates") 19 | self._trusted_certificates = trusted_certificates 20 | 21 | @classmethod 22 | def from_pem(cls, trusted_certificates_as_pem: List[str]) -> "CertificateChainVerifier": 23 | if not trusted_certificates_as_pem: 24 | raise ValueError("Supplied an empty list of trusted certificates") 25 | 26 | return cls([X509(cert_pem) for cert_pem in trusted_certificates_as_pem]) 27 | 28 | @classmethod 29 | def from_file(cls, trusted_certificates_path: Path) -> "CertificateChainVerifier": 30 | parsed_certificates: List[str] = [] 31 | with trusted_certificates_path.open() as file_content: 32 | for pem_segment in file_content.read().split("-----BEGIN CERTIFICATE-----")[1::]: 33 | pem_content = pem_segment.split("-----END CERTIFICATE-----")[0] 34 | pem_cert = f"-----BEGIN CERTIFICATE-----{pem_content}-----END CERTIFICATE-----" 35 | parsed_certificates.append(pem_cert) 36 | 37 | return cls.from_pem(parsed_certificates) 38 | 39 | def verify(self, certificate_chain: List[X509]) -> List[X509]: 40 | """Validate a certificate chain and if successful, return the verified chain. 41 | 42 | The leaf certificate must be at index 0 of the certificate chain. 43 | 44 | WARNING: the validation logic does not perform hostname validation. 45 | """ 46 | if not certificate_chain: 47 | raise ValueError("Supplied an empty certificate chain") 48 | 49 | # Setup the context object for cert verification 50 | store_ctx = X509_STORE_CTX() 51 | store_ctx.set0_trusted_stack(self._trusted_certificates) 52 | store_ctx.set0_untrusted(certificate_chain) 53 | 54 | leaf_cert = certificate_chain[0] 55 | store_ctx.set_cert(leaf_cert) 56 | 57 | # Run the verification 58 | result: int = X509.verify_cert(store_ctx) 59 | if result == 1: 60 | # Validation succeeded 61 | verified_chain = store_ctx.get1_chain() 62 | return verified_chain 63 | elif result == 0: 64 | # Validation failed 65 | verify_result = store_ctx.get_error() 66 | raise CertificateChainVerificationFailed(verify_result) 67 | elif result < 0: 68 | raise RuntimeError("X509_verify_cert() was invoked incorrectly") 69 | else: 70 | raise RuntimeError(f"Result {result}; should never happen according to the OpenSSL documentation") 71 | -------------------------------------------------------------------------------- /tests/openssl_server/server-self-signed-key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQCdFZEQNiFVc0ng 3 | FzM0kku4iOVrwFPs1gP25peKNEFmKCoxLmREjoC9JlPMfTleO3hhuAjE51jpKJGg 4 | iplhs16r4bP9ESz9/x2LliF6w9QrEMBC6YwKeb7XUcso72D2lpYyzhnbqH2O9ndk 5 | xdOzxRN2LLx7LRg3vPwA57DcF7/oQhhfvX5NMKE5dJR+5EAkJ/YeUN6hmWCgLTL/ 6 | 23T2JwYCH9uv6eAS37+ncH2B1feX92J6wKgmJmtaEGbZ/rToAEHGw1XYkDWFi6sB 7 | nvyUXcFwUp+WhLuNQfk6PWrsVjHtifriaZfigr6Cn5WYY/jr/zsMnwTcsT9EnU59 8 | dggnWMLpyRaENQrvPzfabqNjZuC7l0CpkvEGTdEoY+YdYCI2jFyf3aO0eRQSHo0B 9 | 938XaDpqChyq3Y/pODnM6w0R9Inh/aQvaT2m/FYLVZbMW5/G/ihXfZP3BUZ0LKj3 10 | D8hXxXR9fajkHyRivrgxaaqCTIksw4U53cKCd+q2eC6aR+KDRUuUI/uRQ939EOwt 11 | 3MJE72WegFwC/sHeLWFGeoADBYzWv1rfMZt22jy/ekrCi08il+aZKJCpN1cR8FVs 12 | PvUNjYnJdxmMp5titOmHC7oGWuZqzt7x3ldQzvVk4bYCncZBpg1nbmujxz6rSGPK 13 | a1xDZAZttdcA/vXBcAbe2ASdWfpVTwIDAQABAoICAAzQT48E+18fEm2nNtQZAvhA 14 | ooZRoAb3xkcDtGTfsl4E9LwiTqeRAEttrvRFWsKnE0DVZFG7lXMfjhGMfMqNeTGI 15 | Lch8+DCX+O8EBiMfilUg/q32oyfPmpOx5mKmdEBpNENcsJtMeUGKNV8RDB3j+5xu 16 | NcnOeeVCE0R3oeOyRENL8PUOXhkp2Fz5d4uKTvkkEV2TtzVzb4Bhb4GTg0z5DfJq 17 | at9EsrSXb+jWskY/D/1jfrMIuC60f5lPMTSjJFqweq0eDgimatdD92UgdPYdV8Qz 18 | 2TTMCwiQ0yW/ENPKjDQWR1LkzjD5/VRmdCuELDAEF+sFpb3i74hHVLnDQRMEmJGc 19 | yiYU5k4RBFmEx2BlQYeR/TziOw5nmheCEDPHmLcGZEjQD0Bycf4Mexprjfy8SA5D 20 | 2qaeG9ijJ6aIbVA8kCp0yuCL1OaTXaTY3IK3eEwBWs0a27kyBG6BIzxPYc0WVII3 21 | gRcR1aRaU6rIALNa5M3wEFQK/UueQ6K512sRzaBmdCSViMV5yNi6E+ZTSEqA5VxY 22 | pvBlab2lVU4uSJqV7hik4FbakNlPXz+BW0axTgazfsctlS4eC25gJgsSMxvmS2XB 23 | jh0J3zYOYxij8tZFMnvkWmqmfI5TaHPYdWNG1XcTszZH7obwkvZCjWC60PZP2h1e 24 | enDy2mND+u/+ZWFqFjgxAoIBAQDQWes+J/R3VbjRC1GupHQLxYkRW8YM1HjJsvRA 25 | gZAX5o+Cq7fWMGO/I8dMfHWrIrU0IdXHbMBLXHCfXMpw+CBm0yWaaZqO4y0BSjpm 26 | BMBe+Sjtzv19B4RTTX/czdSb4paZpCy2J8Poh63rb5102u5bQokVk4/DxR4jMYPt 27 | xemJWkF0Usu9I8fcOrV7fVKoIHjPFN3nqvsobjWlCZ0ZOrNuwJojwUhQOKUgIT82 28 | Hwbk0RfTJsiIBl7DANUXYcHsPYww12Z4DvCzjHBXPspX/ckazfEsEowDJ/uk1XDq 29 | wL4HEduEgKxb8cmy499tsEBiuTZZfsoBo4FNlajkJ3KIhM/7AoIBAQDBAi6z080h 30 | OBwBuq7Bn/BqNY3nZGBm46Lbb1A1WB7iP+UMuC2XLyBRwZWGT1Tqi/K1kJkYs3QP 31 | Cq0HN/UPSD/2hnaSU5kayVdEDGw518VSVgskXmZXnHtQgsJBVtCKUc3MNEm0zfhi 32 | r9aLLyUHjyx0y0HQc5Z+7tyMDQmmys0Qe9stcIE2veshICn7PfcbIB6azjrQTpNh 33 | hBayFbjWKixd3i92QjJcBLs4mra8G+ZgjZJdJ20HWhDEV/rXmL/tLu43a36VoVPC 34 | rFbSlDh3VP53hf0cCh32ScJ0RT0fHPuRdv+NXOTh3xX+rN6gf/+Erl3tvIQexxZc 35 | Lu/5i2L3Zwu9AoIBAQCHVgEyTK3FXk9AqoOSV4xxoQxZ1C0fZFxZV/7Eb+RzQfZy 36 | QKyXWrNQEyOAEVA1q8PcayX79i4qRY96VMHDA8m4QOsqE/KrYfF35wlr/yYeCuaF 37 | InER5/ISUkL94E18PIDqp/PLFqVww8E7LzMuFo/Bg8Sb2VAHMGFVJvK1XltGNvRR 38 | ZP6mizllWlVMM6mfQAh5KurBcxTVvFDoNQhwvOqUxFLbas9YHJNV/YKb2yeVNiSt 39 | qYGUqd6f+EhyebOHiqsnhqMu1TWQy2alpUm2QkmBOciwghOkcTJKbcJ9GlnVKcpX 40 | lWVgC/yiUNZu34/TEM+27l61FcoF5XLhUKZa+zpFAoIBAQCm+XVf23dWKs2H7XIs 41 | TmrV55jpOxxvRrXosucoDyFAyNgmZdwDNCD4ucna1Rz4gLQrwXnBNdbNAIZqfU2D 42 | uBSl+PPxaWNGGjNlyn2CmRm7PncLMqPFXboND+JwVmO5lkW9SOsPATXGYqrv5Ixb 43 | etCSBhnc9XKYQ5sHimv0IPTBMvWN3QvcSPd2w/WtxpDVpbb0ZD/bYG7+aSCdVk9+ 44 | 8CNL3eEfpOseUnWJ5cb6/AQVOcUK1AXt20wwIJEBHcPEtNofld6AIn5tJ11BdjtX 45 | eW9gBcnQDSyYmn0gy5myJge5c8JoEJpukencVr+PCM0MgzD9cB645bGBdJXlRGIM 46 | +NfdAoIBAQCfMP0QAqa3193YuzS4BC3nDHj6WLb2tPwPqKfQ7M5FCeZdCuYZaIhV 47 | B8gOaYG0KJ7F3DC00kVlmPiNBum+fcSzQXorPF4rCRPjKTowUasqN8naQOtVa8qA 48 | qTWTUKlYHVD9fX7ecc1VtQnnm2/fn8HKUL1fmaglznTH0InLiVdOW+hCUKJEcRRm 49 | uFXIe34ifza8orAGhgRXWm82k9aGSx3A+N9v9QfJPdZTtEPjL6F3LYUWomtSPzsa 50 | puOC/wkNovvGpzX5DW2FLvQgNfB5xKHH3rU2Os+HQBZdAAXtW2luo4QUVVVDGWzb 51 | Vgy18JpsMiYzYQ4t5o6dDel/zmUDFN5/ 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_SSL_SESSION.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | 4 | #include 5 | 6 | #include "nassl_errors.h" 7 | #include "nassl_SSL_SESSION.h" 8 | #include "openssl_utils.h" 9 | 10 | 11 | static PyObject* nassl_SSL_SESSION_new(PyTypeObject *type, PyObject *args, PyObject *kwds) 12 | { 13 | PyErr_SetString(PyExc_NotImplementedError, "Cannot directly create an SSL_SESSION object. Get it from SSL.get_session()"); 14 | return NULL; 15 | } 16 | 17 | 18 | static void nassl_SSL_SESSION_dealloc(nassl_SSL_SESSION_Object *self) 19 | { 20 | if (self->sslSession != NULL) 21 | { 22 | SSL_SESSION_free(self->sslSession); 23 | self->sslSession = NULL; 24 | } 25 | Py_TYPE(self)->tp_free((PyObject*)self); 26 | } 27 | 28 | 29 | static PyObject* nassl_SSL_SESSION_as_text(nassl_SSL_SESSION_Object *self) 30 | { 31 | return generic_print_to_string((int (*)(BIO *, const void *)) &SSL_SESSION_print, self->sslSession); 32 | } 33 | 34 | #ifndef LEGACY_OPENSSL 35 | static PyObject* nassl_SSL_SESSION_set_max_early_data(nassl_SSL_SESSION_Object *self, PyObject *args) 36 | { 37 | int max_early_data = 0; 38 | 39 | if (!PyArg_ParseTuple(args, "I", &max_early_data)) 40 | { 41 | return NULL; 42 | } 43 | 44 | if (self->sslSession != NULL) { 45 | SSL_SESSION_set_max_early_data(self->sslSession, max_early_data); 46 | } 47 | 48 | return Py_BuildValue("I", max_early_data); 49 | } 50 | 51 | static PyObject* nassl_SSL_SESSION_get_max_early_data(nassl_SSL_SESSION_Object *self, PyObject *args) 52 | { 53 | int returnValue = 0; 54 | 55 | if (self->sslSession != NULL) { 56 | returnValue = SSL_SESSION_get_max_early_data(self->sslSession); 57 | } 58 | 59 | return Py_BuildValue("I", returnValue); 60 | } 61 | #endif 62 | 63 | static PyMethodDef nassl_SSL_SESSION_Object_methods[] = 64 | { 65 | {"as_text", (PyCFunction)nassl_SSL_SESSION_as_text, METH_NOARGS, 66 | "OpenSSL's SSL_SESSION_print()." 67 | }, 68 | #ifndef LEGACY_OPENSSL 69 | {"set_max_early_data", (PyCFunction)nassl_SSL_SESSION_set_max_early_data, METH_VARARGS, 70 | "OpenSSL's SSL_SESSION_set_max_early_data()." 71 | }, 72 | {"get_max_early_data", (PyCFunction)nassl_SSL_SESSION_get_max_early_data, METH_NOARGS, 73 | "OpenSSL's SSL_SESSION_get_max_early_data()." 74 | }, 75 | #endif 76 | {NULL} // Sentinel 77 | }; 78 | /* 79 | 80 | static PyMemberDef nassl_SSL_SESSION_Object_members[] = { 81 | {NULL} // Sentinel 82 | }; 83 | */ 84 | 85 | PyTypeObject nassl_SSL_SESSION_Type = 86 | { 87 | PyVarObject_HEAD_INIT(NULL, 0) 88 | "_nassl.SSL_SESSION", /*tp_name*/ 89 | sizeof(nassl_SSL_SESSION_Object), /*tp_basicsize*/ 90 | 0, /*tp_itemsize*/ 91 | (destructor)nassl_SSL_SESSION_dealloc, /*tp_dealloc*/ 92 | 0, /*tp_print*/ 93 | 0, /*tp_getattr*/ 94 | 0, /*tp_setattr*/ 95 | 0, /*tp_compare*/ 96 | 0, /*tp_repr*/ 97 | 0, /*tp_as_number*/ 98 | 0, /*tp_as_sequence*/ 99 | 0, /*tp_as_mapping*/ 100 | 0, /*tp_hash */ 101 | 0, /*tp_call*/ 102 | 0, /*tp_str*/ 103 | 0, /*tp_getattro*/ 104 | 0, /*tp_setattro*/ 105 | 0, /*tp_as_buffer*/ 106 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 107 | "SSL_SESSION objects", /* tp_doc */ 108 | 0, /* tp_traverse */ 109 | 0, /* tp_clear */ 110 | 0, /* tp_richcompare */ 111 | 0, /* tp_weaklistoffset */ 112 | 0, /* tp_iter */ 113 | 0, /* tp_iternext */ 114 | nassl_SSL_SESSION_Object_methods, /* tp_methods */ 115 | 0, /* tp_members */ 116 | 0, /* tp_getset */ 117 | 0, /* tp_base */ 118 | 0, /* tp_dict */ 119 | 0, /* tp_descr_get */ 120 | 0, /* tp_descr_set */ 121 | 0, /* tp_dictoffset */ 122 | 0, /* tp_init */ 123 | 0, /* tp_alloc */ 124 | nassl_SSL_SESSION_new, /* tp_new */ 125 | }; 126 | 127 | 128 | 129 | void module_add_SSL_SESSION(PyObject* m) 130 | { 131 | nassl_SSL_SESSION_Type.tp_new = nassl_SSL_SESSION_new; 132 | if (PyType_Ready(&nassl_SSL_SESSION_Type) < 0) 133 | { 134 | return; 135 | } 136 | 137 | Py_INCREF(&nassl_SSL_SESSION_Type); 138 | PyModule_AddObject(m, "SSL_SESSION", (PyObject *)&nassl_SSL_SESSION_Type); 139 | } 140 | 141 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_BIO.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | 4 | #include 5 | 6 | #include "nassl_BIO.h" 7 | #include "nassl_errors.h" 8 | 9 | 10 | static PyObject* nassl_BIO_new(PyTypeObject *type, PyObject *args, PyObject *kwds) 11 | { 12 | nassl_BIO_Object *self; 13 | BIO *sBio; 14 | 15 | self = (nassl_BIO_Object *)type->tp_alloc(type, 0); 16 | if (self == NULL) 17 | { 18 | return NULL; 19 | } 20 | self->bio = NULL; 21 | 22 | if (!PyArg_ParseTuple(args, "")) 23 | { 24 | Py_DECREF(self); 25 | return NULL; 26 | } 27 | // Only support for BIO pairs for now 28 | sBio = BIO_new(BIO_s_bio()); 29 | if (sBio == NULL) 30 | { 31 | raise_OpenSSL_error(); 32 | Py_DECREF(self); 33 | return NULL; 34 | } 35 | 36 | self->bio = sBio; 37 | return (PyObject *)self; 38 | } 39 | 40 | 41 | static void nassl_BIO_dealloc(nassl_BIO_Object *self) 42 | { 43 | if (self->bio != NULL) 44 | { 45 | // This might be a small memory leak, but the BIOs should implicitly freed by SSL_free() called from 46 | // nassl_SSL_dealloc(); enabling BIO_free here leads to a double free crash 47 | //BIO_free(self->bio); 48 | self->bio = NULL; 49 | } 50 | Py_TYPE(self)->tp_free((PyObject*)self); 51 | } 52 | 53 | 54 | static PyObject* nassl_BIO_make_bio_pair(PyObject *nullPtr, PyObject *args) 55 | { 56 | nassl_BIO_Object *bio1_Object, *bio2_Object = NULL; 57 | if (!PyArg_ParseTuple(args, "O!O!", &nassl_BIO_Type, &bio1_Object, &nassl_BIO_Type, &bio2_Object)) 58 | { 59 | return NULL; 60 | } 61 | (void)BIO_make_bio_pair(bio1_Object->bio, bio2_Object->bio); 62 | Py_RETURN_NONE; 63 | } 64 | 65 | 66 | static PyObject* nassl_BIO_read(nassl_BIO_Object *self, PyObject *args) 67 | { 68 | char *readBuffer; 69 | PyObject *res = NULL; 70 | 71 | unsigned int readSize; 72 | if (!PyArg_ParseTuple(args, "I", &readSize)) 73 | { 74 | return NULL; 75 | } 76 | 77 | readBuffer = (char *) PyMem_Malloc(readSize); 78 | if (readBuffer == NULL) 79 | { 80 | return PyErr_NoMemory(); 81 | } 82 | 83 | if (BIO_read(self->bio, readBuffer, readSize) > 0) 84 | { 85 | res = PyBytes_FromStringAndSize(readBuffer, readSize); 86 | } 87 | else 88 | { 89 | PyErr_SetString(PyExc_IOError, "BIO_read() failed."); 90 | } 91 | 92 | PyMem_Free(readBuffer); 93 | return res; 94 | } 95 | 96 | 97 | static PyObject* nassl_BIO_pending(nassl_BIO_Object *self, PyObject *args) 98 | { 99 | size_t returnValue = BIO_ctrl_pending(self->bio); 100 | return Py_BuildValue("I", returnValue); 101 | } 102 | 103 | 104 | static PyObject* nassl_BIO_write(nassl_BIO_Object *self, PyObject *args) 105 | { 106 | PyObject *res = NULL; 107 | Py_ssize_t writeSize; 108 | int returnValue; 109 | char *writeBuffer; 110 | if (!PyArg_ParseTuple(args, "s#", &writeBuffer, &writeSize)) 111 | { 112 | return NULL; 113 | } 114 | 115 | returnValue = BIO_write(self->bio, writeBuffer, writeSize); 116 | if (returnValue > 0) 117 | { 118 | // Write OK 119 | res = Py_BuildValue("I", returnValue); 120 | } 121 | else 122 | { 123 | // Write failed 124 | // TODO: Error handling 125 | PyErr_SetString(PyExc_IOError, "BIO_write() failed"); 126 | return NULL; 127 | } 128 | return res; 129 | } 130 | 131 | 132 | static PyMethodDef nassl_BIO_Object_methods[] = 133 | { 134 | {"read", (PyCFunction)nassl_BIO_read, METH_VARARGS, 135 | "OpenSSL's BIO_read()." 136 | }, 137 | {"pending", (PyCFunction)nassl_BIO_pending, METH_NOARGS, 138 | "OpenSSL's BIO_ctrl_pending()." 139 | }, 140 | {"write", (PyCFunction)nassl_BIO_write, METH_VARARGS, 141 | "OpenSSL's BIO_write()." 142 | }, 143 | {"make_bio_pair", (PyCFunction)nassl_BIO_make_bio_pair, METH_VARARGS | METH_STATIC, 144 | "OpenSSL's BIO_make_bio_pair()." 145 | }, 146 | {NULL} // Sentinel 147 | }; 148 | 149 | 150 | PyTypeObject nassl_BIO_Type = 151 | { 152 | PyVarObject_HEAD_INIT(NULL, 0) 153 | "_nassl.BIO", /*tp_name*/ 154 | sizeof(nassl_BIO_Object), /*tp_basicsize*/ 155 | 0, /*tp_itemsize*/ 156 | (destructor)nassl_BIO_dealloc, /*tp_dealloc*/ 157 | 0, /*tp_print*/ 158 | 0, /*tp_getattr*/ 159 | 0, /*tp_setattr*/ 160 | 0, /*tp_compare*/ 161 | 0, /*tp_repr*/ 162 | 0, /*tp_as_number*/ 163 | 0, /*tp_as_sequence*/ 164 | 0, /*tp_as_mapping*/ 165 | 0, /*tp_hash */ 166 | 0, /*tp_call*/ 167 | 0, /*tp_str*/ 168 | 0, /*tp_getattro*/ 169 | 0, /*tp_setattro*/ 170 | 0, /*tp_as_buffer*/ 171 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 172 | "BIO objects", /* tp_doc */ 173 | 0, /* tp_traverse */ 174 | 0, /* tp_clear */ 175 | 0, /* tp_richcompare */ 176 | 0, /* tp_weaklistoffset */ 177 | 0, /* tp_iter */ 178 | 0, /* tp_iternext */ 179 | nassl_BIO_Object_methods, /* tp_methods */ 180 | 0, /* tp_members */ 181 | 0, /* tp_getset */ 182 | 0, /* tp_base */ 183 | 0, /* tp_dict */ 184 | 0, /* tp_descr_get */ 185 | 0, /* tp_descr_set */ 186 | 0, /* tp_dictoffset */ 187 | 0, /* tp_init */ 188 | 0, /* tp_alloc */ 189 | nassl_BIO_new, /* tp_new */ 190 | }; 191 | 192 | 193 | 194 | void module_add_BIO(PyObject* m) 195 | { 196 | nassl_BIO_Type.tp_new = nassl_BIO_new; 197 | if (PyType_Ready(&nassl_BIO_Type) < 0) 198 | return; 199 | 200 | Py_INCREF(&nassl_BIO_Type); 201 | PyModule_AddObject(m, "BIO", (PyObject *)&nassl_BIO_Type); 202 | } 203 | 204 | -------------------------------------------------------------------------------- /nassl/ephemeral_key_info.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | from enum import IntEnum 4 | from dataclasses import dataclass, field 5 | from typing import Dict, Set 6 | 7 | 8 | class OpenSslEvpPkeyEnum(IntEnum): 9 | """Maps to the EVP_PKEY_XXX OpenSSL constants (obj_mac.h) used as the temporary key during key exchange.""" 10 | 11 | DH = 28 12 | EC = 408 13 | X25519 = 1034 14 | X448 = 1035 15 | RSA = 6 16 | DSA = 116 17 | RSA_PSS = 912 18 | 19 | 20 | class OpenSslEcNidEnum(IntEnum): 21 | """Maps to NID_XXX values valid for OpenSslEvpPkeyEnum.EC (obj_mac.h). 22 | 23 | Valid values for TLS taken from https://tools.ietf.org/html/rfc4492 and https://tools.ietf.org/html/rfc8422 24 | """ 25 | 26 | # RFC4492 (now deprecated) 27 | SECT163K1 = 721 28 | SECT163R1 = 722 29 | SECT163R2 = 723 30 | SECT193R1 = 724 31 | SECT193R2 = 725 32 | SECT233K1 = 726 33 | SECT233R1 = 727 34 | SECT239K1 = 728 35 | SECT283K1 = 729 36 | SECT283R1 = 730 37 | SECT409K1 = 731 38 | SECT409R1 = 732 39 | SECT571K1 = 733 40 | SECT571R1 = 734 41 | SECP160K1 = 708 42 | SECP160R1 = 709 43 | SECP160R2 = 710 44 | SECP192K1 = 711 45 | SECP224K1 = 712 46 | SECP224R1 = 713 47 | SECP256K1 = 714 48 | 49 | # RFC8422 (current) 50 | SECP192R1 = 409 51 | SECP256R1 = 415 52 | SECP384R1 = 715 53 | SECP521R1 = 716 54 | X25519 = 1034 55 | X448 = 1035 56 | 57 | # Brainpool 58 | brainpoolP160r1 = 921 59 | brainpoolP160t1 = 922 60 | brainpoolP192r1 = 923 61 | brainpoolP192t1 = 924 62 | brainpoolP224r1 = 925 63 | brainpoolP224t1 = 926 64 | brainpoolP256r1 = 927 65 | brainpoolP256t1 = 928 66 | brainpoolP320r1 = 929 67 | brainpoolP320t1 = 930 68 | brainpoolP384r1 = 931 69 | brainpoolP384t1 = 932 70 | brainpoolP512r1 = 933 71 | brainpoolP512t1 = 934 72 | 73 | @classmethod 74 | def get_supported_by_ssl_client(cls) -> Set["OpenSslEcNidEnum"]: 75 | """Some NIDs (the brainpool ones) trigger an error with nassl.SslClient when trying to use them. 76 | 77 | See also https://github.com/nabla-c0d3/nassl/issues/104. 78 | """ 79 | return {nid for nid in cls if "brainpool" not in nid.name} 80 | 81 | 82 | # Mapping between OpenSSL EVP_PKEY_XXX value and display name 83 | _OPENSSL_EVP_PKEY_TO_NAME_MAPPING: Dict[OpenSslEvpPkeyEnum, str] = { 84 | OpenSslEvpPkeyEnum.DH: "DH", 85 | OpenSslEvpPkeyEnum.EC: "ECDH", 86 | OpenSslEvpPkeyEnum.X25519: "ECDH", 87 | OpenSslEvpPkeyEnum.X448: "ECDH", 88 | OpenSslEvpPkeyEnum.RSA: "RSA", 89 | OpenSslEvpPkeyEnum.DSA: "DSA", 90 | OpenSslEvpPkeyEnum.RSA_PSS: "RSA-PSS", 91 | } 92 | 93 | 94 | # Mapping between the OpenSSL NID_XXX value and the SECG name (https://www.rfc-editor.org/rfc/rfc8422.html#appendix-A) 95 | _OPENSSL_NID_TO_SECG_ANSI_X9_62: Dict[OpenSslEcNidEnum, str] = { 96 | OpenSslEcNidEnum.SECT163K1: "sect163k1", 97 | OpenSslEcNidEnum.SECT163R1: "sect163r1", 98 | OpenSslEcNidEnum.SECT163R2: "sect163r2", 99 | OpenSslEcNidEnum.SECT193R1: "sect193r1", 100 | OpenSslEcNidEnum.SECT193R2: "sect193r2", 101 | OpenSslEcNidEnum.SECT233K1: "sect233k1", 102 | OpenSslEcNidEnum.SECT233R1: "sect233r1", 103 | OpenSslEcNidEnum.SECT239K1: "sect239k1", 104 | OpenSslEcNidEnum.SECT283K1: "sect283k1", 105 | OpenSslEcNidEnum.SECT283R1: "sect283r1", 106 | OpenSslEcNidEnum.SECT409K1: "sect409k1", 107 | OpenSslEcNidEnum.SECT409R1: "sect409r1", 108 | OpenSslEcNidEnum.SECT571K1: "sect571k1", 109 | OpenSslEcNidEnum.SECT571R1: "sect571r1", 110 | OpenSslEcNidEnum.SECP160K1: "secp160k1", 111 | OpenSslEcNidEnum.SECP160R1: "secp160r1", 112 | OpenSslEcNidEnum.SECP160R2: "secp160r2", 113 | OpenSslEcNidEnum.SECP192K1: "secp192k1", 114 | OpenSslEcNidEnum.SECP192R1: "secp192r1", 115 | OpenSslEcNidEnum.SECP224K1: "secp224k1", 116 | OpenSslEcNidEnum.SECP224R1: "secp224r1", 117 | OpenSslEcNidEnum.SECP256K1: "secp256k1", 118 | OpenSslEcNidEnum.SECP256R1: "secp256r1", 119 | OpenSslEcNidEnum.SECP384R1: "secp384r1", 120 | OpenSslEcNidEnum.SECP521R1: "secp521r1", 121 | OpenSslEcNidEnum.X25519: "X25519", 122 | OpenSslEcNidEnum.X448: "X448", 123 | OpenSslEcNidEnum.brainpoolP160r1: "brainpoolP160r1", 124 | OpenSslEcNidEnum.brainpoolP160t1: "brainpoolP160t1", 125 | OpenSslEcNidEnum.brainpoolP192r1: "brainpoolP192r1", 126 | OpenSslEcNidEnum.brainpoolP192t1: "brainpoolP192t1", 127 | OpenSslEcNidEnum.brainpoolP224r1: "brainpoolP224r1", 128 | OpenSslEcNidEnum.brainpoolP224t1: "brainpoolP224t1", 129 | OpenSslEcNidEnum.brainpoolP256r1: "brainpoolP256r1", 130 | OpenSslEcNidEnum.brainpoolP256t1: "brainpoolP256t1", 131 | OpenSslEcNidEnum.brainpoolP320r1: "brainpoolP320r1", 132 | OpenSslEcNidEnum.brainpoolP320t1: "brainpoolP320t1", 133 | OpenSslEcNidEnum.brainpoolP384r1: "brainpoolP384r1", 134 | OpenSslEcNidEnum.brainpoolP384t1: "brainpoolP384t1", 135 | OpenSslEcNidEnum.brainpoolP512r1: "brainpoolP512r1", 136 | OpenSslEcNidEnum.brainpoolP512t1: "brainpoolP512t1", 137 | } 138 | 139 | 140 | @dataclass(frozen=True) 141 | class EphemeralKeyInfo(ABC): 142 | """Common fields shared by all kinds of TLS key exchanges.""" 143 | 144 | type: OpenSslEvpPkeyEnum 145 | type_name: str = field(init=False) 146 | size: int 147 | public_bytes: bytearray 148 | 149 | def __post_init__(self) -> None: 150 | # Required because of frozen=True; https://docs.python.org/3/library/dataclasses.html#frozen-instances 151 | object.__setattr__( 152 | self, 153 | "type_name", 154 | _OPENSSL_EVP_PKEY_TO_NAME_MAPPING.get(self.type, "UNKNOWN"), 155 | ) 156 | 157 | 158 | @dataclass(frozen=True) 159 | class EcDhEphemeralKeyInfo(EphemeralKeyInfo): 160 | curve: OpenSslEcNidEnum 161 | curve_name: str = field(init=False) 162 | 163 | def __post_init__(self) -> None: 164 | super().__post_init__() 165 | curve_name = _OPENSSL_NID_TO_SECG_ANSI_X9_62.get(self.curve, f"unknown-curve-with-openssl-id-{self.curve}") 166 | # Required because of frozen=True; https://docs.python.org/3/library/dataclasses.html#frozen-instances 167 | object.__setattr__(self, "curve_name", curve_name) 168 | 169 | 170 | @dataclass(frozen=True) 171 | class NistEcDhKeyExchangeInfo(EcDhEphemeralKeyInfo): 172 | x: bytearray 173 | y: bytearray 174 | 175 | 176 | @dataclass(frozen=True) 177 | class DhEphemeralKeyInfo(EphemeralKeyInfo): 178 | prime: bytearray 179 | generator: bytearray 180 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import sys 3 | from pathlib import Path 4 | 5 | from build_tasks import ( 6 | ModernOpenSslBuildConfig, 7 | ZlibBuildConfig, 8 | LegacyOpenSslBuildConfig, 9 | SupportedPlatformEnum, 10 | CURRENT_PLATFORM, 11 | ) 12 | from nassl import __author__, __version__ 13 | from setuptools import setup, Extension, find_packages 14 | 15 | SHOULD_BUILD_FOR_DEBUG = False 16 | 17 | 18 | NASSL_SETUP = { 19 | "name": "nassl", 20 | "version": __version__, 21 | "packages": find_packages(exclude=["docs", "tests"]), 22 | "package_data": {"nassl": ["py.typed", "_nassl.pyi", "_nassl_legacy.pyi"]}, 23 | "py_modules": [ 24 | "nassl.__init__", 25 | "nassl.ssl_client", 26 | "nassl.ephemeral_key_info", 27 | "nassl.legacy_ssl_client", 28 | "nassl.ocsp_response", 29 | "nassl.cert_chain_verifier", 30 | ], 31 | "description": "Experimental OpenSSL wrapper for Python 3.9+ and SSLyze.", 32 | "author": __author__, 33 | "author_email": "nabla.c0d3@gmail.com", 34 | "url": "https://github.com/nabla-c0d3/nassl", 35 | "python_requires": ">=3.9", 36 | "classifiers": [ 37 | "Development Status :: 4 - Beta", 38 | "Intended Audience :: Developers", 39 | "Intended Audience :: System Administrators", 40 | "Natural Language :: French", 41 | "License :: OSI Approved :: GNU Affero General Public License v3", 42 | "Programming Language :: Python :: 3.9", 43 | "Programming Language :: Python :: 3.10", 44 | "Programming Language :: Python :: 3.11", 45 | "Programming Language :: Python :: 3.12", 46 | "Programming Language :: Python :: 3.13", 47 | "Topic :: System :: Networking", 48 | "Topic :: System :: Monitoring", 49 | "Topic :: System :: Networking :: Monitoring", 50 | "Topic :: Security", 51 | ], 52 | "keywords": "ssl tls scan security library", 53 | } 54 | 55 | # There are two native extensions: the "legacy" OpenSSL one and the "modern" OpenSSL one 56 | # First setup the common settings for both legacy and modern nassl 57 | BASE_NASSL_EXT_SETUP = { 58 | "extra_compile_args": [], 59 | "extra_link_args": [], 60 | "sources": [ 61 | "nassl/_nassl/nassl.c", 62 | "nassl/_nassl/nassl_SSL_CTX.c", 63 | "nassl/_nassl/nassl_SSL.c", 64 | "nassl/_nassl/nassl_X509.c", 65 | "nassl/_nassl/nassl_errors.c", 66 | "nassl/_nassl/nassl_BIO.c", 67 | "nassl/_nassl/nassl_SSL_SESSION.c", 68 | "nassl/_nassl/openssl_utils.c", 69 | "nassl/_nassl/nassl_OCSP_RESPONSE.c", 70 | "nassl/_nassl/python_utils.c", 71 | ], 72 | } 73 | 74 | if CURRENT_PLATFORM in [ 75 | SupportedPlatformEnum.WINDOWS_32, 76 | SupportedPlatformEnum.WINDOWS_64, 77 | ]: 78 | # Build using the Python that was used to run this script; will not work for cross-compiling 79 | PYTHON_LIBS_PATH = Path(sys.executable).parent / "libs" 80 | 81 | BASE_NASSL_EXT_SETUP.update( 82 | { 83 | "library_dirs": [str(PYTHON_LIBS_PATH)], 84 | "libraries": [ 85 | "user32", 86 | "kernel32", 87 | "Gdi32", 88 | "Advapi32", 89 | "Ws2_32", 90 | "crypt32", 91 | ], 92 | } 93 | ) 94 | else: 95 | BASE_NASSL_EXT_SETUP["extra_compile_args"].append("-Wall") 96 | 97 | if CURRENT_PLATFORM in SupportedPlatformEnum.all_linux_platforms(): 98 | # Hide internal OpenSSL symbols to avoid "symbol confusion" when Python loads the system's OpenSSL libraries 99 | # https://github.com/nabla-c0d3/nassl/issues/95 100 | BASE_NASSL_EXT_SETUP["extra_link_args"].append("-Wl,--exclude-libs=ALL") 101 | 102 | if CURRENT_PLATFORM == SupportedPlatformEnum.LINUX_64: 103 | # Explicitly disable executable stack on Linux 64 to address issues with Ubuntu on Windows 104 | # https://github.com/nabla-c0d3/nassl/issues/28 105 | BASE_NASSL_EXT_SETUP["extra_link_args"].append("-Wl,-z,noexecstack") 106 | 107 | zlib_config = ZlibBuildConfig(CURRENT_PLATFORM) 108 | 109 | 110 | # The configure the setup for legacy nassl 111 | legacy_openssl_config = LegacyOpenSslBuildConfig(CURRENT_PLATFORM) 112 | 113 | LEGACY_NASSL_EXT_SETUP = copy.deepcopy(BASE_NASSL_EXT_SETUP) 114 | LEGACY_NASSL_EXT_SETUP["name"] = "nassl._nassl_legacy" 115 | LEGACY_NASSL_EXT_SETUP["define_macros"] = [("LEGACY_OPENSSL", "1")] 116 | LEGACY_NASSL_EXT_SETUP.update( 117 | { 118 | "include_dirs": [str(legacy_openssl_config.include_path)], 119 | "extra_objects": [ 120 | # The order matters on some flavors of Linux 121 | str(legacy_openssl_config.libssl_path), 122 | str(legacy_openssl_config.libcrypto_path), 123 | str(zlib_config.libz_path), 124 | ], 125 | } 126 | ) 127 | 128 | # The configure the setup for modern nassl 129 | modern_openssl_config = ModernOpenSslBuildConfig(CURRENT_PLATFORM) 130 | 131 | MODERN_NASSL_EXT_SETUP = copy.deepcopy(BASE_NASSL_EXT_SETUP) 132 | MODERN_NASSL_EXT_SETUP["name"] = "nassl._nassl" 133 | MODERN_NASSL_EXT_SETUP.update( 134 | { 135 | "include_dirs": [str(modern_openssl_config.include_path)], 136 | "extra_objects": [ 137 | # The order matters on some flavors of Linux 138 | str(modern_openssl_config.libssl_path), 139 | str(modern_openssl_config.libcrypto_path), 140 | str(zlib_config.libz_path), 141 | ], 142 | } 143 | ) 144 | MODERN_NASSL_EXT_SETUP["sources"].append("nassl/_nassl/nassl_X509_STORE_CTX.c") # API only available in modern nassl 145 | 146 | 147 | if CURRENT_PLATFORM in [ 148 | SupportedPlatformEnum.WINDOWS_32, 149 | SupportedPlatformEnum.WINDOWS_64, 150 | ]: 151 | if SHOULD_BUILD_FOR_DEBUG: 152 | LEGACY_NASSL_EXT_SETUP.update({"extra_compile_args": ["/Zi"], "extra_link_args": ["/DEBUG"]}) 153 | MODERN_NASSL_EXT_SETUP.update({"extra_compile_args": ["/Zi"], "extra_link_args": ["/DEBUG"]}) 154 | else: 155 | # Add arguments specific to Unix builds 156 | LEGACY_NASSL_EXT_SETUP["include_dirs"].append(str(Path("nassl") / "_nassl")) 157 | MODERN_NASSL_EXT_SETUP["include_dirs"].append(str(Path("nassl") / "_nassl")) 158 | 159 | 160 | NASSL_SETUP.update( 161 | { 162 | "ext_modules": [ 163 | Extension(**LEGACY_NASSL_EXT_SETUP), 164 | Extension(**MODERN_NASSL_EXT_SETUP), 165 | ] 166 | } 167 | ) 168 | 169 | 170 | if __name__ == "__main__": 171 | setup(**NASSL_SETUP) 172 | -------------------------------------------------------------------------------- /tests/cert_chain_verifier_test.py: -------------------------------------------------------------------------------- 1 | import ssl 2 | from pathlib import Path 3 | from typing import List 4 | 5 | import pytest 6 | 7 | from nassl.cert_chain_verifier import ( 8 | CertificateChainVerifier, 9 | CertificateChainVerificationFailed, 10 | ) 11 | from nassl._nassl import X509 12 | 13 | 14 | @pytest.fixture 15 | def certificate_chain_as_x509() -> List[X509]: 16 | leaf_pem = ssl.get_server_certificate(("www.github.com", 443)) 17 | 18 | # Sectigo ECC Domain Validation Secure Server CA 19 | intermediate_pem = """-----BEGIN CERTIFICATE----- 20 | MIIDqDCCAy6gAwIBAgIRAPNkTmtuAFAjfglGvXvh9R0wCgYIKoZIzj0EAwMwgYgx 21 | CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtKZXJz 22 | ZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYDVQQD 23 | EyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE4MTEw 24 | MjAwMDAwMFoXDTMwMTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAkdCMRswGQYDVQQI 25 | ExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGDAWBgNVBAoT 26 | D1NlY3RpZ28gTGltaXRlZDE3MDUGA1UEAxMuU2VjdGlnbyBFQ0MgRG9tYWluIFZh 27 | bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEH 28 | A0IABHkYk8qfbZ5sVwAjBTcLXw9YWsTef1Wj6R7W2SUKiKAgSh16TwUwimNJE4xk 29 | IQeV/To14UrOkPAY9z2vaKb71EijggFuMIIBajAfBgNVHSMEGDAWgBQ64QmG1M8Z 30 | wpZ2dEl23OA1xmNjmjAdBgNVHQ4EFgQU9oUKOxGG4QR9DqoLLNLuzGR7e64wDgYD 31 | VR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYIKwYB 32 | BQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEwUAYD 33 | VR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1c3QuY29tL1VTRVJUcnVz 34 | dEVDQ0NlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYGCCsGAQUFBwEBBGowaDA/ 35 | BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3QuY29tL1VTRVJUcnVzdEVD 36 | Q0FkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1 37 | c3QuY29tMAoGCCqGSM49BAMDA2gAMGUCMEvnx3FcsVwJbZpCYF9z6fDWJtS1UVRs 38 | cS0chWBNKPFNpvDKdrdKRe+oAkr2jU+ubgIxAODheSr2XhcA7oz9HmedGdMhlrd9 39 | 4ToKFbZl+/OnFFzqnvOhcjHvClECEQcKmc8fmA== 40 | -----END CERTIFICATE----- 41 | """ 42 | return [X509(leaf_pem), X509(intermediate_pem)] 43 | 44 | 45 | class TestCertificateChainVerifier: 46 | def test_valid_certificate_chain(self, certificate_chain_as_x509): 47 | path_validator = CertificateChainVerifier.from_file(Path(__file__).absolute().parent / "mozilla.pem") 48 | path_validator.verify(certificate_chain_as_x509) 49 | 50 | def test_expired_certificate_chain(self): 51 | expired_leaf = """-----BEGIN CERTIFICATE----- 52 | MIIFSzCCBDOgAwIBAgIQSueVSfqavj8QDxekeOFpCTANBgkqhkiG9w0BAQsFADCB 53 | kDELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G 54 | A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxNjA0BgNV 55 | BAMTLUNPTU9ETyBSU0EgRG9tYWluIFZhbGlkYXRpb24gU2VjdXJlIFNlcnZlciBD 56 | QTAeFw0xNTA0MDkwMDAwMDBaFw0xNTA0MTIyMzU5NTlaMFkxITAfBgNVBAsTGERv 57 | bWFpbiBDb250cm9sIFZhbGlkYXRlZDEdMBsGA1UECxMUUG9zaXRpdmVTU0wgV2ls 58 | ZGNhcmQxFTATBgNVBAMUDCouYmFkc3NsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQAD 59 | ggEPADCCAQoCggEBAMIE7PiM7gTCs9hQ1XBYzJMY61yoaEmwIrX5lZ6xKyx2PmzA 60 | S2BMTOqytMAPgLaw+XLJhgL5XEFdEyt/ccRLvOmULlA3pmccYYz2QULFRtMWhyef 61 | dOsKnRFSJiFzbIRMeVXk0WvoBj1IFVKtsyjbqv9u/2CVSndrOfEk0TG23U3AxPxT 62 | uW1CrbV8/q71FdIzSOciccfCFHpsKOo3St/qbLVytH5aohbcabFXRNsKEqveww9H 63 | dFxBIuGa+RuT5q0iBikusbpJHAwnnqP7i/dAcgCskgjZjFeEU4EFy+b+a1SYQCeF 64 | xxC7c3DvaRhBB0VVfPlkPz0sw6l865MaTIbRyoUCAwEAAaOCAdUwggHRMB8GA1Ud 65 | IwQYMBaAFJCvajqUWgvYkOoSVnPfQ7Q6KNrnMB0GA1UdDgQWBBSd7sF7gQs6R2lx 66 | GH0RN5O8pRs/+zAOBgNVHQ8BAf8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUE 67 | FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQIC 68 | BzArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAI 69 | BgZngQwBAgEwVAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5jb21vZG9jYS5j 70 | b20vQ09NT0RPUlNBRG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNybDCB 71 | hQYIKwYBBQUHAQEEeTB3ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LmNvbW9kb2Nh 72 | LmNvbS9DT01PRE9SU0FEb21haW5WYWxpZGF0aW9uU2VjdXJlU2VydmVyQ0EuY3J0 73 | MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wIwYDVR0RBBww 74 | GoIMKi5iYWRzc2wuY29tggpiYWRzc2wuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBq 75 | evHa/wMHcnjFZqFPRkMOXxQhjHUa6zbgH6QQFezaMyV8O7UKxwE4PSf9WNnM6i1p 76 | OXy+l+8L1gtY54x/v7NMHfO3kICmNnwUW+wHLQI+G1tjWxWrAPofOxkt3+IjEBEH 77 | fnJ/4r+3ABuYLyw/zoWaJ4wQIghBK4o+gk783SHGVnRwpDTysUCeK1iiWQ8dSO/r 78 | ET7BSp68ZVVtxqPv1dSWzfGuJ/ekVxQ8lEEFeouhN0fX9X3c+s5vMaKwjOrMEpsi 79 | 8TRwz311SotoKQwe6Zaoz7ASH1wq7mcvf71z81oBIgxw+s1F73hczg36TuHvzmWf 80 | RwxPuzZEaFZcVlmtqoq8 81 | -----END CERTIFICATE-----""" 82 | 83 | expired_intermediate = """-----BEGIN CERTIFICATE----- 84 | MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB 85 | hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G 86 | A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV 87 | BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy 88 | MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT 89 | EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR 90 | Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh 91 | bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP 92 | ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh 93 | bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0 94 | Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6 95 | ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51 96 | UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n 97 | c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY 98 | MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz 99 | 30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV 100 | HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG 101 | BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv 102 | bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB 103 | AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E 104 | T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v 105 | ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p 106 | mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/ 107 | e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps 108 | P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY 109 | dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc 110 | 2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG 111 | V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4 112 | HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX 113 | j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII 114 | 0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap 115 | lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf 116 | +AZxAeKCINT+b72x 117 | -----END CERTIFICATE-----""" 118 | 119 | path_validator = CertificateChainVerifier.from_file(Path(__file__).absolute().parent / "mozilla.pem") 120 | 121 | try: 122 | path_validator.verify([X509(expired_leaf), X509(expired_intermediate)]) 123 | except CertificateChainVerificationFailed as e: 124 | assert 10 == e.openssl_error_code # 10 is the error code for an expired certificate 125 | assert e.openssl_error_string 126 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_X509.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | 4 | // Fix symbol clashing on Windows 5 | // https://bugs.launchpad.net/pyopenssl/+bug/570101 6 | #ifdef _WIN32 7 | #include "winsock.h" 8 | #endif 9 | 10 | #include 11 | #include 12 | 13 | 14 | #include "nassl_errors.h" 15 | #include "nassl_X509.h" 16 | #include "openssl_utils.h" 17 | 18 | #ifndef LEGACY_OPENSSL 19 | #include "nassl_X509_STORE_CTX.h" 20 | #endif 21 | 22 | 23 | // nassl.X509.new() 24 | static PyObject* nassl_X509_new(PyTypeObject *type, PyObject *args, PyObject *kwds) 25 | { 26 | nassl_X509_Object *self; 27 | char *pemCertificate; 28 | BIO *bio; 29 | 30 | self = (nassl_X509_Object *)type->tp_alloc(type, 0); 31 | if (self == NULL) 32 | { 33 | return NULL; 34 | } 35 | 36 | // Read the certificate as PEM and create an X509 object 37 | if (!PyArg_ParseTuple(args, "s", &pemCertificate)) 38 | { 39 | return NULL; 40 | } 41 | 42 | bio = BIO_new(BIO_s_mem()); 43 | BIO_puts(bio, pemCertificate); 44 | 45 | self->x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL); 46 | BIO_vfree(bio); 47 | 48 | if (self->x509 == NULL) 49 | { 50 | PyErr_SetString(PyExc_ValueError, "Could not parse the supplied PEM certificate"); 51 | return NULL; 52 | } 53 | return (PyObject *)self; 54 | } 55 | 56 | 57 | static void nassl_X509_dealloc(nassl_X509_Object *self) 58 | { 59 | if (self->x509 != NULL) 60 | { 61 | X509_free(self->x509); 62 | self->x509 = NULL; 63 | } 64 | Py_TYPE(self)->tp_free((PyObject*)self); 65 | } 66 | 67 | 68 | static PyObject* nassl_X509_as_text(nassl_X509_Object *self, PyObject *args) 69 | { 70 | return generic_print_to_string((int (*)(BIO *, const void *)) &X509_print, self->x509); 71 | } 72 | 73 | 74 | static PyObject* nassl_X509_as_pem(nassl_X509_Object *self, PyObject *args) 75 | { 76 | return generic_print_to_string((int (*)(BIO *, const void *)) &PEM_write_bio_X509, self->x509); 77 | } 78 | 79 | 80 | static PyObject* nassl_X509_verify_cert_error_string(PyObject *nullPtr, PyObject *args) 81 | { 82 | const char *errorString = NULL; 83 | long verifyError = 0; 84 | if (!PyArg_ParseTuple(args, "l", &verifyError)) 85 | { 86 | return NULL; 87 | } 88 | 89 | errorString = X509_verify_cert_error_string(verifyError); 90 | return PyUnicode_FromString(errorString); 91 | } 92 | 93 | 94 | #ifndef LEGACY_OPENSSL 95 | static PyObject* nassl_X509_verify_cert(PyObject *nullPtr, PyObject *args) 96 | { 97 | int verifyReturnValue = 0; 98 | nassl_X509_STORE_CTX_Object *x509storeCtx_PyObject = NULL; 99 | if (!PyArg_ParseTuple(args, "O!", &nassl_X509_STORE_CTX_Type, &x509storeCtx_PyObject)) 100 | { 101 | return NULL; 102 | } 103 | 104 | verifyReturnValue = X509_verify_cert(x509storeCtx_PyObject->x509storeCtx); 105 | return Py_BuildValue("i", verifyReturnValue); 106 | } 107 | #endif 108 | 109 | 110 | static PyMethodDef nassl_X509_Object_methods[] = 111 | { 112 | {"as_text", (PyCFunction)nassl_X509_as_text, METH_NOARGS, 113 | "Returns a string containing the result of OpenSSL's X509_print()." 114 | }, 115 | {"as_pem", (PyCFunction)nassl_X509_as_pem, METH_NOARGS, 116 | "OpenSSL's PEM_write_bio_X509()." 117 | }, 118 | {"verify_cert_error_string", (PyCFunction)nassl_X509_verify_cert_error_string, METH_VARARGS | METH_STATIC, 119 | "OpenSSL's X509_verify_cert_error_string()." 120 | }, 121 | #ifndef LEGACY_OPENSSL 122 | {"verify_cert", (PyCFunction)nassl_X509_verify_cert, METH_VARARGS | METH_STATIC, 123 | "OpenSSL's X509_verify_cert()." 124 | }, 125 | #endif 126 | 127 | {NULL} // Sentinel 128 | }; 129 | 130 | 131 | PyTypeObject nassl_X509_Type = 132 | { 133 | PyVarObject_HEAD_INIT(NULL, 0) 134 | "_nassl.X509", /*tp_name*/ 135 | sizeof(nassl_X509_Object), /*tp_basicsize*/ 136 | 0, /*tp_itemsize*/ 137 | (destructor)nassl_X509_dealloc, /*tp_dealloc*/ 138 | 0, /*tp_print*/ 139 | 0, /*tp_getattr*/ 140 | 0, /*tp_setattr*/ 141 | 0, /*tp_compare*/ 142 | 0, /*tp_repr*/ 143 | 0, /*tp_as_number*/ 144 | 0, /*tp_as_sequence*/ 145 | 0, /*tp_as_mapping*/ 146 | 0, /*tp_hash */ 147 | 0, /*tp_call*/ 148 | 0, /*tp_str*/ 149 | 0, /*tp_getattro*/ 150 | 0, /*tp_setattro*/ 151 | 0, /*tp_as_buffer*/ 152 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 153 | "X509 objects", /* tp_doc */ 154 | 0, /* tp_traverse */ 155 | 0, /* tp_clear */ 156 | 0, /* tp_richcompare */ 157 | 0, /* tp_weaklistoffset */ 158 | 0, /* tp_iter */ 159 | 0, /* tp_iternext */ 160 | nassl_X509_Object_methods, /* tp_methods */ 161 | 0, /* tp_members */ 162 | 0, /* tp_getset */ 163 | 0, /* tp_base */ 164 | 0, /* tp_dict */ 165 | 0, /* tp_descr_get */ 166 | 0, /* tp_descr_set */ 167 | 0, /* tp_dictoffset */ 168 | 0, /* tp_init */ 169 | 0, /* tp_alloc */ 170 | nassl_X509_new, /* tp_new */ 171 | }; 172 | 173 | 174 | 175 | void module_add_X509(PyObject* m) 176 | { 177 | nassl_X509_Type.tp_new = nassl_X509_new; 178 | if (PyType_Ready(&nassl_X509_Type) < 0) 179 | { 180 | return; 181 | } 182 | 183 | Py_INCREF(&nassl_X509_Type); 184 | PyModule_AddObject(m, "X509", (PyObject *)&nassl_X509_Type); 185 | 186 | } 187 | 188 | 189 | // Utility function to to convert a stack of OpenSSL X509s to a Python list of nassl.X509 190 | PyObject* stackOfX509ToPyList(STACK_OF(X509) *certChain) 191 | { 192 | PyObject* certChainPyList = NULL; 193 | int certChainCount = 0, i = 0; 194 | 195 | certChainCount = sk_X509_num(certChain); 196 | certChainPyList = PyList_New(certChainCount); 197 | if (certChainPyList == NULL) 198 | { 199 | return PyErr_NoMemory(); 200 | } 201 | 202 | for (i=0; ix509 = cert; 222 | 223 | // Add the X509 object to the final list 224 | PyList_SET_ITEM(certChainPyList, i, (PyObject *)x509_Object); 225 | } 226 | return certChainPyList; 227 | } 228 | -------------------------------------------------------------------------------- /nassl/legacy_ssl_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from pathlib import Path 3 | 4 | from nassl._nassl import WantReadError, WantX509LookupError 5 | 6 | from nassl.ssl_client import ( 7 | ClientCertificateRequested, 8 | OpenSslVersionEnum, 9 | OpenSslVerifyEnum, 10 | OpenSslFileTypeEnum, 11 | BaseSslClient, 12 | ) 13 | from typing import List 14 | from typing import Optional 15 | import sys 16 | 17 | 18 | from nassl import _nassl_legacy # type: ignore 19 | 20 | 21 | class LegacySslClient(BaseSslClient): 22 | """An insecure SSL client with additional debug methods that no one should ever use (insecure renegotiation, etc.).""" 23 | 24 | # The legacy client uses the legacy OpenSSL 25 | _NASSL_MODULE = _nassl_legacy 26 | 27 | def __init__( 28 | self, 29 | underlying_socket: Optional[socket.socket] = None, 30 | ssl_version: OpenSslVersionEnum = OpenSslVersionEnum.SSLV23, 31 | ssl_verify: OpenSslVerifyEnum = OpenSslVerifyEnum.PEER, 32 | ssl_verify_locations: Optional[Path] = None, 33 | client_certificate_chain: Optional[Path] = None, 34 | client_key: Optional[Path] = None, 35 | client_key_type: OpenSslFileTypeEnum = OpenSslFileTypeEnum.PEM, 36 | client_key_password: str = "", 37 | ignore_client_authentication_requests: bool = False, 38 | ) -> None: 39 | super().__init__( 40 | underlying_socket, 41 | ssl_version, 42 | ssl_verify, 43 | ssl_verify_locations, 44 | client_certificate_chain, 45 | client_key, 46 | client_key_type, 47 | client_key_password, 48 | ignore_client_authentication_requests, 49 | ) 50 | 51 | # Specific servers do not reply to a client hello that is bigger than 255 bytes 52 | # See http://rt.openssl.org/Ticket/Display.html?id=2771&user=guest&pass=guest 53 | # So we make the default cipher list smaller (to make the client hello smaller) 54 | if ssl_version != OpenSslVersionEnum.SSLV2: # This makes SSLv2 fail 55 | self._ssl.set_cipher_list("HIGH:-aNULL:-eNULL:-3DES:-SRP:-PSK:-CAMELLIA") 56 | else: 57 | # Handshake workaround for SSL2 + IIS 7 58 | # TODO(AD): Provide a built-in mechansim for overriding the handshake logic 59 | self.do_handshake = self.do_ssl2_iis_handshake # type: ignore 60 | 61 | def get_secure_renegotiation_support(self) -> bool: 62 | return self._ssl.get_secure_renegotiation_support() 63 | 64 | def get_current_compression_method(self) -> Optional[str]: 65 | return self._ssl.get_current_compression_method() 66 | 67 | @staticmethod 68 | def get_available_compression_methods() -> List[str]: 69 | """Returns the list of SSL compression methods supported by SslClient.""" 70 | return _nassl_legacy.SSL.get_available_compression_methods() 71 | 72 | def do_renegotiate(self) -> None: 73 | """Initiate an SSL renegotiation.""" 74 | if not self._is_handshake_completed: 75 | raise IOError("SSL Handshake was not completed; cannot renegotiate.") 76 | 77 | self._ssl.renegotiate() 78 | self.do_handshake() 79 | 80 | _SSL_MODE_SEND_FALLBACK_SCSV = 0x00000080 81 | 82 | def enable_fallback_scsv(self) -> None: 83 | self._ssl.set_mode(self._SSL_MODE_SEND_FALLBACK_SCSV) 84 | 85 | # TODO(AD): Allow the handshake method to be overridden instead of this 86 | def do_ssl2_iis_handshake(self) -> None: 87 | if self._sock is None: 88 | # TODO: Auto create a socket ? 89 | raise IOError("Internal socket set to None; cannot perform handshake.") 90 | 91 | while True: 92 | try: 93 | self._ssl.do_handshake() 94 | self._is_handshake_completed = True 95 | # Handshake was successful 96 | return 97 | 98 | except WantReadError: 99 | # OpenSSL is expecting more data from the peer 100 | # Send available handshake data to the peer 101 | lengh_to_read = self._network_bio.pending() 102 | while lengh_to_read: 103 | # Get the data from the SSL engine 104 | handshake_data_out = self._network_bio.read(lengh_to_read) 105 | 106 | if "SSLv2 read server verify A" in self._ssl.state_string_long(): 107 | # Awful h4ck for SSLv2 when connecting to IIS7 (like in the 90s) 108 | # OpenSSL sends the client's CMK and data message in the same packet without 109 | # waiting for the server's response, causing IIS 7 to hang on the connection. 110 | # This workaround forces our client to send the CMK message, then wait for the server's 111 | # response, and then send the data packet 112 | # if '\x02' in handshake_data_out[2]: # Make sure we're looking at the CMK message 113 | message_type = handshake_data_out[2] 114 | IS_PYTHON_2 = sys.version_info < (3, 0) 115 | if IS_PYTHON_2: 116 | message_type = ord(message_type) 117 | 118 | if message_type == 2: # Make sure we're looking at the CMK message 119 | # cmk_size = handshake_data_out[0:2] 120 | if IS_PYTHON_2: 121 | first_byte = ord(handshake_data_out[0]) 122 | second_byte = ord(handshake_data_out[1]) 123 | else: 124 | first_byte = int(handshake_data_out[0]) 125 | second_byte = int(handshake_data_out[1]) 126 | first_byte = (first_byte & 0x7F) << 8 127 | size = first_byte + second_byte 128 | # Manually split the two records to force them to be sent separately 129 | cmk_packet = handshake_data_out[0 : size + 2] # noqa: E203 130 | data_packet = handshake_data_out[size + 2 : :] # noqa: E203 131 | self._sock.send(cmk_packet) 132 | 133 | handshake_data_in = self._sock.recv(self._DEFAULT_BUFFER_SIZE) 134 | # print repr(handshake_data_in) 135 | if len(handshake_data_in) == 0: 136 | raise IOError("Nassl SSL handshake failed: peer did not send data back.") 137 | # Pass the data to the SSL engine 138 | self._network_bio.write(handshake_data_in) 139 | handshake_data_out = data_packet 140 | 141 | # Send it to the peer 142 | self._sock.send(handshake_data_out) 143 | lengh_to_read = self._network_bio.pending() 144 | 145 | handshake_data_in = self._sock.recv(self._DEFAULT_BUFFER_SIZE) 146 | if len(handshake_data_in) == 0: 147 | raise IOError("Nassl SSL handshake failed: peer did not send data back.") 148 | # Pass the data to the SSL engine 149 | self._network_bio.write(handshake_data_in) 150 | 151 | except WantX509LookupError: 152 | # Server asked for a client certificate and we didn't provide one 153 | raise ClientCertificateRequested(self.get_client_CA_list()) 154 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_OCSP_RESPONSE.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | 4 | // Fix symbol clashing on Windows 5 | // https://bugs.launchpad.net/pyopenssl/+bug/570101 6 | #ifdef _WIN32 7 | #include "winsock.h" 8 | #endif 9 | 10 | #include 11 | #include 12 | 13 | #include "python_utils.h" 14 | #include "nassl_errors.h" 15 | #include "nassl_OCSP_RESPONSE.h" 16 | 17 | 18 | static PyObject* nassl_OCSP_RESPONSE_new(PyTypeObject *type, PyObject *args, PyObject *kwds) 19 | { 20 | PyErr_SetString(PyExc_NotImplementedError, "Cannot directly create an OCSP_RESPONSE object. Get it from SSL.get_tlsext_status_ocsp_resp()"); 21 | return NULL; 22 | } 23 | 24 | 25 | static void nassl_OCSP_RESPONSE_dealloc(nassl_OCSP_RESPONSE_Object *self) 26 | { 27 | if (self->ocspResp != NULL) 28 | { 29 | OCSP_RESPONSE_free(self->ocspResp); 30 | self->ocspResp = NULL; 31 | } 32 | if (self->peerCertChain != NULL) 33 | { 34 | /*int i = 0; 35 | int certNum = sk_X509_num(self->peerCertChain); 36 | for(i=0;ipeerCertChain, &X509_free); 38 | } */ 39 | sk_X509_free(self->peerCertChain); 40 | self->peerCertChain = NULL; 41 | } 42 | Py_TYPE(self)->tp_free((PyObject*)self); 43 | } 44 | 45 | 46 | static PyObject* nassl_OCSP_RESPONSE_as_text(nassl_OCSP_RESPONSE_Object *self) 47 | { 48 | PyObject* ocsResp_PyString = NULL; 49 | BIO *memBio = NULL; 50 | unsigned int txtLen = 0; 51 | char *txtBuffer = NULL; 52 | 53 | // Print the OCSP response to a memory BIO 54 | memBio = BIO_new(BIO_s_mem()); 55 | if (memBio == NULL) 56 | { 57 | return raise_OpenSSL_error(); 58 | } 59 | 60 | OCSP_RESPONSE_print(memBio, self->ocspResp, 0); 61 | 62 | // Extract the text from the BIO 63 | txtLen = BIO_pending(memBio); 64 | txtBuffer = (char *) PyMem_Malloc(txtLen); 65 | if (txtBuffer == NULL) 66 | { 67 | BIO_vfree(memBio); 68 | return PyErr_NoMemory(); 69 | } 70 | 71 | BIO_read(memBio, txtBuffer, txtLen); 72 | // An OCSP response may contain non-utf8 characters (if there are certificates in it) so we return it as bytes 73 | // To handle decoding errors in Python 74 | ocsResp_PyString = PyBytes_FromStringAndSize(txtBuffer, txtLen); 75 | PyMem_Free(txtBuffer); 76 | BIO_vfree(memBio); 77 | 78 | return ocsResp_PyString; 79 | } 80 | 81 | 82 | static PyObject* nassl_OCSP_RESPONSE_as_der_bytes(nassl_OCSP_RESPONSE_Object *self) 83 | { 84 | PyObject *res = NULL; 85 | char *ocspBuf = NULL; 86 | unsigned int ocspRespLen = 0; 87 | 88 | ocspRespLen = i2d_OCSP_RESPONSE(self->ocspResp, (unsigned char **) &ocspBuf); 89 | if (ocspRespLen < 0) 90 | { 91 | PyErr_SetString(PyExc_ValueError, "Could not convert OCSP response do DER bytes"); 92 | return NULL; 93 | } 94 | res = PyBytes_FromStringAndSize(ocspBuf, ocspRespLen); 95 | OPENSSL_free(ocspBuf); 96 | return res; 97 | } 98 | 99 | 100 | static PyObject* nassl_OCSP_RESPONSE_basic_verify(nassl_OCSP_RESPONSE_Object *self, PyObject *args) 101 | { 102 | X509_STORE *trustedCAs = NULL; 103 | int certNum = 0, verifyRes = 0, i = 0, respStatus = 0; 104 | OCSP_BASICRESP *basicResp = NULL; 105 | char *caFilePath = NULL; 106 | if (PyArg_ParseFilePath(args, &caFilePath) == NULL) 107 | { 108 | return NULL; 109 | } 110 | 111 | // Ensure the response that can be verified 112 | respStatus = OCSP_response_status(self->ocspResp); 113 | if (respStatus != OCSP_RESPONSE_STATUS_SUCCESSFUL) 114 | { 115 | PyErr_SetString(PyExc_ValueError, "Cannot verify an OCSP response with a non-successful status"); 116 | return NULL; 117 | } 118 | 119 | // Load the file containing the trusted CA certs 120 | trustedCAs = X509_STORE_new(); 121 | if (trustedCAs == NULL) 122 | { 123 | return raise_OpenSSL_error(); 124 | } 125 | 126 | X509_STORE_load_locations(trustedCAs, caFilePath, NULL); 127 | 128 | // Verify the OCSP response 129 | basicResp = OCSP_response_get1_basic(self->ocspResp); 130 | 131 | // Add the server's certificate chain to the OCSP response. Is this correct ? 132 | // Maybe ? http://www.mail-archive.com/openssl-users@openssl.org/msg70201.html 133 | certNum = sk_X509_num(self->peerCertChain); 134 | for(i=0; ipeerCertChain, i); 137 | OCSP_basic_add1_cert(basicResp, cert); 138 | } 139 | 140 | verifyRes = OCSP_basic_verify(basicResp, NULL, trustedCAs, 0); 141 | OCSP_BASICRESP_free(basicResp); 142 | X509_STORE_free(trustedCAs); 143 | if (verifyRes <= 0) 144 | { 145 | return raise_OpenSSL_error(); 146 | } 147 | Py_RETURN_NONE; 148 | } 149 | 150 | 151 | static PyMethodDef nassl_OCSP_RESPONSE_Object_methods[] = 152 | { 153 | {"as_text", (PyCFunction)nassl_OCSP_RESPONSE_as_text, METH_NOARGS, 154 | "OpenSSL's OCSP_RESPONSE_print()." 155 | }, 156 | {"as_der_bytes", (PyCFunction)nassl_OCSP_RESPONSE_as_der_bytes, METH_NOARGS, 157 | "OpenSSL's i2d_OCSP_RESPONSE()." 158 | }, 159 | {"basic_verify", (PyCFunction)nassl_OCSP_RESPONSE_basic_verify, METH_VARARGS, 160 | "OpenSSL's OCSP_basic_verify()." 161 | }, 162 | {NULL} // Sentinel 163 | }; 164 | 165 | 166 | PyTypeObject nassl_OCSP_RESPONSE_Type = 167 | { 168 | PyVarObject_HEAD_INIT(NULL, 0) 169 | "_nassl.OCSP_RESPONSE", /*tp_name*/ 170 | sizeof(nassl_OCSP_RESPONSE_Object), /*tp_basicsize*/ 171 | 0, /*tp_itemsize*/ 172 | (destructor)nassl_OCSP_RESPONSE_dealloc, /*tp_dealloc*/ 173 | 0, /*tp_print*/ 174 | 0, /*tp_getattr*/ 175 | 0, /*tp_setattr*/ 176 | 0, /*tp_compare*/ 177 | 0, /*tp_repr*/ 178 | 0, /*tp_as_number*/ 179 | 0, /*tp_as_sequence*/ 180 | 0, /*tp_as_mapping*/ 181 | 0, /*tp_hash */ 182 | 0, /*tp_call*/ 183 | 0, /*tp_str*/ 184 | 0, /*tp_getattro*/ 185 | 0, /*tp_setattro*/ 186 | 0, /*tp_as_buffer*/ 187 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 188 | "OCSP_RESPONSE objects", /* tp_doc */ 189 | 0, /* tp_traverse */ 190 | 0, /* tp_clear */ 191 | 0, /* tp_richcompare */ 192 | 0, /* tp_weaklistoffset */ 193 | 0, /* tp_iter */ 194 | 0, /* tp_iternext */ 195 | nassl_OCSP_RESPONSE_Object_methods, /* tp_methods */ 196 | 0, /* tp_members */ 197 | 0, /* tp_getset */ 198 | 0, /* tp_base */ 199 | 0, /* tp_dict */ 200 | 0, /* tp_descr_get */ 201 | 0, /* tp_descr_set */ 202 | 0, /* tp_dictoffset */ 203 | 0, /* tp_init */ 204 | 0, /* tp_alloc */ 205 | nassl_OCSP_RESPONSE_new, /* tp_new */ 206 | }; 207 | 208 | 209 | void module_add_OCSP_RESPONSE(PyObject* m) 210 | { 211 | nassl_OCSP_RESPONSE_Type.tp_new = nassl_OCSP_RESPONSE_new; 212 | if (PyType_Ready(&nassl_OCSP_RESPONSE_Type) < 0) 213 | { 214 | return; 215 | } 216 | 217 | Py_INCREF(&nassl_OCSP_RESPONSE_Type); 218 | PyModule_AddObject(m, "OCSP_RESPONSE", (PyObject *)&nassl_OCSP_RESPONSE_Type); 219 | } 220 | -------------------------------------------------------------------------------- /tests/SSL_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from nassl import _nassl 4 | from nassl import _nassl_legacy 5 | from nassl.ssl_client import SslClient, OpenSslVersionEnum, OpenSslVerifyEnum 6 | 7 | 8 | @pytest.mark.parametrize("nassl_module", [_nassl, _nassl_legacy]) 9 | class TestCommonSSL: 10 | def test_new(self, nassl_module): 11 | nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 12 | 13 | def test_new_bad(self, nassl_module): 14 | # Invalid None SSL_CTX 15 | with pytest.raises(TypeError): 16 | nassl_module.SSL(None) 17 | 18 | def test_set_verify(self, nassl_module): 19 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 20 | test_ssl.set_verify(OpenSslVerifyEnum.PEER.value) 21 | 22 | def test_set_verify_bad(self, nassl_module): 23 | # Invalid verify constant 24 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 25 | with pytest.raises(ValueError): 26 | test_ssl.set_verify(1235) 27 | 28 | def test_set_bio(self, nassl_module): 29 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 30 | test_bio = nassl_module.BIO() 31 | test_ssl.set_bio(test_bio) 32 | 33 | def test_set_bio_bad(self, nassl_module): 34 | # Invalid None BIO 35 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 36 | with pytest.raises(TypeError): 37 | test_ssl.set_bio(None) 38 | 39 | def test_set_connect_state(self, nassl_module): 40 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 41 | test_ssl.set_connect_state() 42 | 43 | # Can't really unittest a full handshake, read or write 44 | def test_do_handshake_bad(self, nassl_module): 45 | # Connection type not set 46 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 47 | with pytest.raises(_nassl.OpenSSLError, match="connection type not set"): 48 | test_ssl.do_handshake() 49 | 50 | def test_pending(self, nassl_module): 51 | # No BIO attached to the SSL object 52 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 53 | assert 0 == test_ssl.pending() 54 | 55 | def test_get_secure_renegotiation_support(self, nassl_module): 56 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 57 | test_ssl.get_secure_renegotiation_support() 58 | 59 | def test_get_current_compression_method(self, nassl_module): 60 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 61 | test_ssl.get_current_compression_method() 62 | 63 | def test_get_available_compression_methods_has_zlib(self, nassl_module): 64 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 65 | assert ["zlib compression"] == test_ssl.get_available_compression_methods() 66 | 67 | def test_set_tlsext_host_name(self, nassl_module): 68 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 69 | test_ssl.set_tlsext_host_name("tests") 70 | 71 | def test_set_tlsext_host_name_bad(self, nassl_module): 72 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 73 | with pytest.raises(TypeError): 74 | test_ssl.set_tlsext_host_name(None) 75 | 76 | def test_set_cipher_list(self, nassl_module): 77 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 78 | test_ssl.set_cipher_list("HIGH") 79 | 80 | def test_shutdown_bad(self, nassl_module): 81 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 82 | with pytest.raises(_nassl.OpenSSLError, match="uninitialized"): 83 | test_ssl.shutdown() 84 | 85 | def test_get_cipher_list(self, nassl_module): 86 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 87 | assert test_ssl.get_cipher_list() 88 | 89 | def test_get_cipher_name(self, nassl_module): 90 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 91 | test_ssl.get_cipher_name() 92 | 93 | def test_get_cipher_bits(self, nassl_module): 94 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 95 | assert 0 == test_ssl.get_cipher_bits() 96 | 97 | def test_get_client_CA_list_bad(self, nassl_module): 98 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 99 | assert [] == test_ssl.get_client_CA_list() 100 | 101 | def test_get_verify_result(self, nassl_module): 102 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 103 | assert 0 == test_ssl.get_verify_result() 104 | 105 | def test_renegotiate(self, nassl_module): 106 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 107 | test_ssl.renegotiate() 108 | 109 | def test_get_session(self, nassl_module): 110 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 111 | test_ssl.get_session() 112 | 113 | def test_set_session_bad(self, nassl_module): 114 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 115 | with pytest.raises(TypeError): 116 | test_ssl.set_session(None) 117 | 118 | def test_set_options_bad(self, nassl_module): 119 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 120 | assert 0 <= test_ssl.set_options(123) 121 | 122 | def test_set_tlsext_status_type(self, nassl_module): 123 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 124 | test_ssl.set_tlsext_status_type(SslClient._TLSEXT_STATUSTYPE_ocsp) 125 | 126 | def test_get_tlsext_status_type(self, nassl_module): 127 | test_ssl = nassl_module.SSL(nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 128 | assert None is test_ssl.get_tlsext_status_ocsp_resp() 129 | 130 | 131 | class TestModernSSL: 132 | def test_set_ciphersuites_bad_string(self): 133 | # Invalid cipher string 134 | test_ssl = _nassl.SSL(_nassl.SSL_CTX(OpenSslVersionEnum.TLSV1_2.value)) 135 | with pytest.raises(_nassl.OpenSSLError, match="no cipher match"): 136 | test_ssl.set_ciphersuites("lol") 137 | 138 | 139 | class TestLegacySSL: 140 | # The following tests don't pass with modern OpenSSL - the API might have changed 141 | def test_set_cipher_list_bad(self): 142 | # Invalid cipher string 143 | test_ssl = _nassl_legacy.SSL(_nassl_legacy.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 144 | with pytest.raises(_nassl.OpenSSLError): 145 | test_ssl.set_cipher_list("badcipherstring") 146 | 147 | def test_do_handshake_bad_eof(self): 148 | # No BIO attached to the SSL object 149 | test_ssl = _nassl_legacy.SSL(_nassl_legacy.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 150 | test_ssl.set_connect_state() 151 | with pytest.raises(_nassl.SslError, match="An EOF was observed that violates the protocol"): 152 | test_ssl.do_handshake() 153 | 154 | def test_read_bad(self): 155 | # No BIO attached to the SSL object 156 | test_ssl = _nassl_legacy.SSL(_nassl_legacy.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 157 | test_ssl.set_connect_state() 158 | with pytest.raises(_nassl.OpenSSLError, match="ssl handshake failure"): 159 | test_ssl.read(128) 160 | 161 | def test_write_bad(self): 162 | # No BIO attached to the SSL object 163 | test_ssl = _nassl_legacy.SSL(_nassl_legacy.SSL_CTX(OpenSslVersionEnum.SSLV23.value)) 164 | test_ssl.set_connect_state() 165 | with pytest.raises(_nassl.OpenSSLError, match="ssl handshake failure"): 166 | test_ssl.write("tests") 167 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_errors.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #ifdef _WIN32 5 | #define PyErr_SetFromErrGeneric(x) PyErr_SetExcFromWindowsErr(x, 0) 6 | #else 7 | #define PyErr_SetFromErrGeneric(x) PyErr_SetFromErrno(x) 8 | #endif 9 | 10 | #include "nassl_errors.h" 11 | 12 | 13 | PyObject *nassl_OpenSSLError_Exception; 14 | static PyObject *nassl_SslError_Exception; 15 | static PyObject *nassl_WantReadError_Exception; 16 | static PyObject *nassl_WantWriteError_Exception; 17 | static PyObject *nassl_WantX509LookupError_Exception; 18 | 19 | 20 | PyObject* raise_OpenSSL_error() 21 | { 22 | PyObject *pyFinalErrorString = NULL; 23 | PyObject *pyNewLineString = NULL; 24 | unsigned long iterateOpenSslError = 0; 25 | 26 | pyFinalErrorString = PyUnicode_FromString(""); 27 | if (pyFinalErrorString == NULL) 28 | { 29 | return PyErr_NoMemory(); 30 | } 31 | 32 | pyNewLineString = PyUnicode_FromString("\n"); 33 | if (pyNewLineString == NULL) 34 | { 35 | return PyErr_NoMemory(); 36 | } 37 | 38 | // Just queue all the errors in the error queue to create a giant error string 39 | // TODO: Improve error handling so we only return one single error; no sure if OpenSSL allows that... 40 | iterateOpenSslError = ERR_get_error(); 41 | while(iterateOpenSslError != 0) 42 | { 43 | PyObject *oldPyFinalErrorString = NULL; 44 | // Get the current error string 45 | char *iterateErrorString = ERR_error_string(iterateOpenSslError, NULL); // This includes a NUL character 46 | PyObject *pyIterateErrorString = PyUnicode_FromString(iterateErrorString); 47 | if (pyIterateErrorString == NULL) 48 | { 49 | return PyErr_NoMemory(); 50 | } 51 | 52 | // Add it to our final error 53 | oldPyFinalErrorString = pyFinalErrorString; 54 | pyFinalErrorString = PyUnicode_Concat(pyFinalErrorString, pyIterateErrorString); 55 | if (pyFinalErrorString == NULL) 56 | { 57 | return PyErr_NoMemory(); 58 | } 59 | Py_DECREF(oldPyFinalErrorString); 60 | 61 | // Add a new line 62 | oldPyFinalErrorString = pyFinalErrorString; 63 | pyFinalErrorString = PyUnicode_Concat(pyFinalErrorString, pyNewLineString); 64 | if (pyFinalErrorString == NULL) 65 | { 66 | return PyErr_NoMemory(); 67 | } 68 | Py_DECREF(oldPyFinalErrorString); 69 | 70 | Py_DECREF(pyIterateErrorString); 71 | iterateOpenSslError = ERR_get_error(); 72 | } 73 | 74 | PyErr_SetString(nassl_OpenSSLError_Exception, PyUnicode_AsUTF8(pyFinalErrorString)); 75 | Py_DECREF(pyFinalErrorString); 76 | Py_DECREF(pyNewLineString); 77 | return NULL; 78 | } 79 | 80 | 81 | PyObject* raise_OpenSSL_ssl_error(SSL *ssl, int returnValue) 82 | { 83 | // TODO: Better error handling 84 | int sslError = SSL_get_error(ssl, returnValue); 85 | switch(sslError) 86 | { 87 | case SSL_ERROR_NONE: 88 | break; 89 | 90 | case SSL_ERROR_SSL: 91 | return raise_OpenSSL_error(); 92 | 93 | case SSL_ERROR_SYSCALL: 94 | if (ERR_peek_error() == 0) 95 | { 96 | if (returnValue == 0) 97 | { 98 | PyErr_SetString(nassl_SslError_Exception, "An EOF was observed that violates the protocol"); 99 | return NULL; 100 | } 101 | else if (returnValue == -1) 102 | { 103 | PyErr_SetFromErrGeneric(nassl_SslError_Exception); 104 | return NULL; 105 | } 106 | else 107 | { 108 | PyErr_SetString(nassl_SslError_Exception, "SSL_ERROR_SYSCALL"); 109 | return NULL; 110 | } 111 | } 112 | else 113 | { 114 | return raise_OpenSSL_error(); 115 | } 116 | 117 | case SSL_ERROR_ZERO_RETURN: 118 | PyErr_SetString(nassl_SslError_Exception, "Connection was shut down by peer"); 119 | return NULL; 120 | 121 | case SSL_ERROR_WANT_WRITE: 122 | PyErr_SetString(nassl_WantWriteError_Exception, ""); 123 | return NULL; 124 | 125 | case SSL_ERROR_WANT_READ: 126 | PyErr_SetString(nassl_WantReadError_Exception, ""); 127 | return NULL; 128 | 129 | case SSL_ERROR_WANT_X509_LOOKUP: 130 | PyErr_SetString(nassl_WantX509LookupError_Exception, ""); 131 | return NULL; 132 | 133 | default: 134 | PyErr_SetString(nassl_SslError_Exception, "TODO: Better error handling"); 135 | return NULL; 136 | } 137 | Py_RETURN_NONE; 138 | } 139 | 140 | 141 | int module_add_errors(PyObject* m) 142 | { 143 | // We want both the modern and legacy nassl to use the same exceptions 144 | #ifdef LEGACY_OPENSSL 145 | // In the legacy _nassl, we import these exceptions from the modern _nassl module 146 | PyObject* modern_nassl_module = PyImport_ImportModule("nassl._nassl"); 147 | if (!modern_nassl_module) 148 | { 149 | PyErr_SetString(PyExc_RuntimeError, "Could not import _nassl"); 150 | return 0; 151 | } 152 | 153 | nassl_OpenSSLError_Exception = PyDict_GetItemString(PyModule_GetDict(modern_nassl_module), "OpenSSLError"); 154 | if (!nassl_OpenSSLError_Exception) 155 | { 156 | PyErr_SetString(PyExc_RuntimeError, "Could not import OpenSSLError from _nassl"); 157 | return 0; 158 | } 159 | 160 | nassl_SslError_Exception = PyDict_GetItemString(PyModule_GetDict(modern_nassl_module), "SslError"); 161 | if (!nassl_SslError_Exception) 162 | { 163 | PyErr_SetString(PyExc_RuntimeError, "Could not import SslError from _nassl"); 164 | return 0; 165 | } 166 | 167 | nassl_WantWriteError_Exception = PyDict_GetItemString(PyModule_GetDict(modern_nassl_module), "WantWriteError"); 168 | if (!nassl_WantWriteError_Exception) 169 | { 170 | PyErr_SetString(PyExc_RuntimeError, "Could not import WantWriteError from _nassl"); 171 | return 0; 172 | } 173 | 174 | nassl_WantReadError_Exception = PyDict_GetItemString(PyModule_GetDict(modern_nassl_module), "WantReadError"); 175 | if (!nassl_WantReadError_Exception) 176 | { 177 | PyErr_SetString(PyExc_RuntimeError, "Could not import WantReadError from _nassl"); 178 | return 0; 179 | } 180 | 181 | nassl_WantX509LookupError_Exception = PyDict_GetItemString(PyModule_GetDict(modern_nassl_module), "WantX509LookupError"); 182 | if (!nassl_WantX509LookupError_Exception) 183 | { 184 | PyErr_SetString(PyExc_RuntimeError, "Could not import WantX509LookupError from _nassl"); 185 | return 0; 186 | } 187 | #else 188 | // In the modern _nassl, we define these exceptions 189 | nassl_OpenSSLError_Exception = PyErr_NewException("nassl._nassl.OpenSSLError", NULL, NULL); 190 | Py_INCREF(nassl_OpenSSLError_Exception); 191 | PyModule_AddObject(m, "OpenSSLError", nassl_OpenSSLError_Exception); 192 | 193 | nassl_SslError_Exception = PyErr_NewException("nassl._nassl.SslError", nassl_OpenSSLError_Exception, NULL); 194 | Py_INCREF(nassl_SslError_Exception); 195 | PyModule_AddObject(m, "SslError", nassl_SslError_Exception); 196 | 197 | nassl_WantWriteError_Exception = PyErr_NewException("nassl._nassl.WantWriteError", nassl_SslError_Exception, NULL); 198 | Py_INCREF(nassl_WantWriteError_Exception); 199 | PyModule_AddObject(m, "WantWriteError", nassl_WantWriteError_Exception); 200 | 201 | nassl_WantReadError_Exception = PyErr_NewException("nassl._nassl.WantReadError", nassl_SslError_Exception, NULL); 202 | Py_INCREF(nassl_WantReadError_Exception); 203 | PyModule_AddObject(m, "WantReadError", nassl_WantReadError_Exception); 204 | 205 | nassl_WantX509LookupError_Exception = PyErr_NewException("nassl._nassl.WantX509LookupError", nassl_SslError_Exception, NULL); 206 | Py_INCREF(nassl_WantX509LookupError_Exception); 207 | PyModule_AddObject(m, "WantX509LookupError", nassl_WantX509LookupError_Exception); 208 | #endif 209 | return 1; 210 | } 211 | -------------------------------------------------------------------------------- /tests/openssl_server/__init__.py: -------------------------------------------------------------------------------- 1 | import shlex 2 | 3 | import subprocess 4 | from abc import ABC, abstractmethod 5 | from enum import Enum 6 | 7 | import logging 8 | import time 9 | from pathlib import Path 10 | from threading import Thread 11 | from typing import Optional, List 12 | 13 | from build_tasks import ( 14 | ModernOpenSslBuildConfig, 15 | LegacyOpenSslBuildConfig, 16 | CURRENT_PLATFORM, 17 | SupportedPlatformEnum, 18 | ) 19 | 20 | 21 | _logger = logging.getLogger(name="tests.openssl_server") 22 | 23 | 24 | class ClientAuthConfigEnum(Enum): 25 | """Whether the server asked for client authentication.""" 26 | 27 | DISABLED = 1 28 | OPTIONAL = 2 29 | REQUIRED = 3 30 | 31 | 32 | class _OpenSslServerIOManager: 33 | """Thread to log all output from s_server and reply to incoming connections.""" 34 | 35 | def __init__(self, s_server_stdout, s_server_stdin): 36 | self.s_server_stdout = s_server_stdout 37 | self.s_server_stdin = s_server_stdin 38 | self.is_server_ready = False 39 | 40 | def read_and_log_and_reply(): 41 | while True: 42 | s_server_out = self.s_server_stdout.readline() 43 | if s_server_out: 44 | _logger.warning(f"s_server output: {s_server_out}") 45 | 46 | if b"ACCEPT" in s_server_out: 47 | # S_server is ready to receive connections 48 | self.is_server_ready = True 49 | # Send some data to stdin; required on Windows to jump start modern OpenSSL's s_server 50 | self.s_server_stdin.write(b"\n") 51 | self.s_server_stdin.flush() 52 | 53 | if _OpenSslServer.HELLO_MSG in s_server_out: 54 | # When receiving the special message, we want s_server to reply 55 | self.s_server_stdin.write(b"Hey there") 56 | self.s_server_stdin.flush() 57 | else: 58 | break 59 | 60 | self.thread = Thread(target=read_and_log_and_reply, args=()) 61 | self.thread.daemon = True 62 | self.thread.start() 63 | 64 | def close(self): 65 | pass 66 | # TODO(AD): This hangs on Linux; figure it out 67 | # self.s_server_stdout.close() 68 | # self.s_server_stdin.close() 69 | # self.thread.join() 70 | 71 | 72 | class _OpenSslServer(ABC): 73 | """A wrapper around OpenSSL's s_server CLI.""" 74 | 75 | # On Windows with modern OpenSSL, trying to use ports below 10k will fail for some reason 76 | _AVAILABLE_LOCAL_PORTS = set(range(18110, 18150)) 77 | 78 | _S_SERVER_CMD = ( 79 | "{openssl} s_server -cert {server_cert} -key {server_key} -accept {port}" 80 | ' -cipher "{cipher}" {verify_arg} {extra_args}' 81 | ) 82 | 83 | _ROOT_PATH = Path(__file__).parent.absolute() 84 | 85 | # Client authentication - files generated using https://gist.github.com/nabla-c0d3/c2c5799a84a4867e5cbae42a5c43f89a 86 | _CLIENT_CA_PATH = _ROOT_PATH / "client-ca.pem" 87 | 88 | # A special message clients can send to get a reply from s_server 89 | HELLO_MSG = b"Hello\r\n" 90 | 91 | @classmethod 92 | def get_server_certificate_path(cls) -> Path: 93 | return cls._ROOT_PATH / "server-self-signed-cert.pem" 94 | 95 | @classmethod 96 | def get_server_key_path(cls) -> Path: 97 | return cls._ROOT_PATH / "server-self-signed-key.pem" 98 | 99 | @classmethod 100 | def get_client_certificate_path(cls) -> Path: 101 | return cls._ROOT_PATH / "client-cert.pem" 102 | 103 | @classmethod 104 | def get_client_key_path(cls) -> Path: 105 | return cls._ROOT_PATH / "client-key.pem" 106 | 107 | @classmethod 108 | @abstractmethod 109 | def get_openssl_path(cls) -> Path: 110 | pass 111 | 112 | @classmethod 113 | @abstractmethod 114 | def get_verify_argument(cls, client_auth_config: ClientAuthConfigEnum) -> str: 115 | pass 116 | 117 | def __init__( 118 | self, 119 | client_auth_config: ClientAuthConfigEnum = ClientAuthConfigEnum.DISABLED, 120 | extra_openssl_args: List[str] = [], 121 | cipher: Optional[str] = None, 122 | ) -> None: 123 | self.hostname = "localhost" 124 | self.ip_address = "127.0.0.1" 125 | 126 | # Retrieve one of the available local ports; set.pop() is thread safe 127 | self.port = self._AVAILABLE_LOCAL_PORTS.pop() 128 | self._process = None 129 | self._server_io_manager = None 130 | final_cipher = cipher if cipher else "ALL:COMPLEMENTOFALL" 131 | 132 | self._command_line = self._S_SERVER_CMD.format( 133 | openssl=self.get_openssl_path(), 134 | server_key=self.get_server_key_path(), 135 | server_cert=self.get_server_certificate_path(), 136 | port=self.port, 137 | verify_arg=self.get_verify_argument(client_auth_config), 138 | extra_args=" ".join(extra_openssl_args), 139 | cipher=final_cipher, 140 | ) 141 | 142 | def __enter__(self): 143 | _logger.warning(f'Running s_server with command: "{self._command_line}"') 144 | if CURRENT_PLATFORM in [ 145 | SupportedPlatformEnum.WINDOWS_64, 146 | SupportedPlatformEnum.WINDOWS_32, 147 | ]: 148 | args = self._command_line 149 | else: 150 | args = shlex.split(self._command_line) 151 | try: 152 | self._process = subprocess.Popen( 153 | args, 154 | stdin=subprocess.PIPE, 155 | stdout=subprocess.PIPE, 156 | stderr=subprocess.STDOUT, 157 | ) 158 | self._server_io_manager = _OpenSslServerIOManager(self._process.stdout, self._process.stdin) 159 | 160 | # Block until s_server is ready to accept requests 161 | attempts_count = 0 162 | while not self._server_io_manager.is_server_ready: 163 | time.sleep(1) 164 | attempts_count += 1 165 | 166 | if self._process.poll() is not None or attempts_count > 3: 167 | # s_server has terminated early 168 | raise RuntimeError("Could not start s_server") 169 | 170 | except Exception: 171 | self._terminate_process() 172 | raise 173 | 174 | return self 175 | 176 | def __exit__(self, *args): 177 | self._terminate_process() 178 | return False 179 | 180 | def _terminate_process(self) -> None: 181 | if self._server_io_manager: 182 | self._server_io_manager.close() 183 | self._server_io_manager = None 184 | 185 | if self._process and self._process.poll() is None: 186 | self._process.terminate() 187 | self._process.wait() 188 | self._process = None 189 | 190 | # Free the port that was used; not thread safe but should be fine 191 | self._AVAILABLE_LOCAL_PORTS.add(self.port) 192 | 193 | 194 | class LegacyOpenSslServer(_OpenSslServer): 195 | """A wrapper around the OpenSSL 1.0.0e s_server binary.""" 196 | 197 | def __init__( 198 | self, 199 | client_auth_config: ClientAuthConfigEnum = ClientAuthConfigEnum.DISABLED, 200 | cipher: Optional[str] = None, 201 | prefer_server_order: bool = False, 202 | ) -> None: 203 | extra_args = [] 204 | 205 | if prefer_server_order: 206 | extra_args.append("-serverpref") 207 | 208 | super().__init__(client_auth_config, extra_args, cipher) 209 | 210 | @classmethod 211 | def get_openssl_path(cls): 212 | return LegacyOpenSslBuildConfig(CURRENT_PLATFORM).exe_path 213 | 214 | @classmethod 215 | def get_verify_argument(cls, client_auth_config: ClientAuthConfigEnum) -> str: 216 | options = { 217 | ClientAuthConfigEnum.DISABLED: "", 218 | ClientAuthConfigEnum.OPTIONAL: f"-verify {cls._CLIENT_CA_PATH}", 219 | ClientAuthConfigEnum.REQUIRED: f"-Verify {cls._CLIENT_CA_PATH}", 220 | } 221 | return options[client_auth_config] 222 | 223 | 224 | class ModernOpenSslServer(_OpenSslServer): 225 | """A wrapper around the OpenSSL 1.1.1 s_server binary.""" 226 | 227 | @classmethod 228 | def get_openssl_path(cls): 229 | return ModernOpenSslBuildConfig(CURRENT_PLATFORM).exe_path 230 | 231 | def get_verify_argument(cls, client_auth_config: ClientAuthConfigEnum) -> str: 232 | # The verify argument has subtly changed in OpenSSL 1.1.1 233 | options = { 234 | ClientAuthConfigEnum.DISABLED: "", 235 | ClientAuthConfigEnum.OPTIONAL: f"-verify 1 {cls._CLIENT_CA_PATH}", 236 | ClientAuthConfigEnum.REQUIRED: f"-Verify 1 {cls._CLIENT_CA_PATH}", 237 | } 238 | return options[client_auth_config] 239 | 240 | def __init__( 241 | self, 242 | client_auth_config: ClientAuthConfigEnum = ClientAuthConfigEnum.DISABLED, 243 | max_early_data: Optional[int] = None, 244 | cipher: Optional[str] = None, 245 | prefer_server_order: bool = False, 246 | groups: Optional[str] = None, 247 | ) -> None: 248 | extra_args = [] 249 | 250 | if prefer_server_order: 251 | extra_args.append("-serverpref") 252 | 253 | if groups: 254 | extra_args.append(f"-groups {groups}") 255 | 256 | if max_early_data is not None: 257 | # Enable TLS 1.3 early data on the server 258 | extra_args += ["-early_data", f"-max_early_data {max_early_data}"] 259 | 260 | super().__init__(client_auth_config, extra_args, cipher) 261 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_X509_STORE_CTX.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | 4 | // Fix symbol clashing on Windows 5 | // https://bugs.launchpad.net/pyopenssl/+bug/570101 6 | #ifdef _WIN32 7 | #include "winsock.h" 8 | #endif 9 | 10 | 11 | #include 12 | #include "nassl_X509_STORE_CTX.h" 13 | #include "nassl_X509.h" 14 | 15 | 16 | static PyObject* nassl_X509_STORE_CTX_new(PyTypeObject *type, PyObject *args, PyObject *kwds) 17 | { 18 | nassl_X509_STORE_CTX_Object *self; 19 | 20 | self = (nassl_X509_STORE_CTX_Object *)type->tp_alloc(type, 0); 21 | if (self == NULL) 22 | { 23 | return NULL; 24 | } 25 | 26 | self->x509storeCtx = X509_STORE_CTX_new(); 27 | if (self->x509storeCtx == NULL) 28 | { 29 | PyErr_SetString(PyExc_ValueError, "Could not initialize context"); 30 | return NULL; 31 | } 32 | X509_STORE_CTX_init(self->x509storeCtx, NULL, NULL, NULL); 33 | 34 | self->trustedCertificates = NULL; 35 | self->untrustedCertificates = NULL; 36 | self->leafCertificate = NULL; 37 | 38 | return (PyObject *)self; 39 | } 40 | 41 | 42 | static void nassl_X509_STORE_CTX_dealloc(nassl_X509_STORE_CTX_Object *self) 43 | { 44 | if (self->x509storeCtx != NULL) 45 | { 46 | // First free the "related" OpenSSL structures 47 | if (self->trustedCertificates != NULL) 48 | { 49 | sk_X509_pop_free(self->trustedCertificates, X509_free); 50 | self->trustedCertificates = NULL; 51 | } 52 | 53 | if (self->untrustedCertificates != NULL) 54 | { 55 | sk_X509_pop_free(self->untrustedCertificates, X509_free); 56 | self->untrustedCertificates = NULL; 57 | } 58 | 59 | if (self->leafCertificate != NULL) 60 | { 61 | X509_free(self->leafCertificate); 62 | self->leafCertificate = NULL; 63 | } 64 | 65 | // Then free the actual object 66 | X509_STORE_CTX_free(self->x509storeCtx); 67 | self->x509storeCtx = NULL; 68 | } 69 | Py_TYPE(self)->tp_free((PyObject*)self); 70 | } 71 | 72 | 73 | static STACK_OF(X509) *parseCertificateList(PyObject *args) 74 | { 75 | int i = 0; 76 | Py_ssize_t certsCount = 0; 77 | PyObject *pyListOfX509Objects; 78 | nassl_X509_Object *x509Object; 79 | STACK_OF(X509) *parsedCertificates = sk_X509_new_null(); 80 | 81 | // Parse the Python list 82 | if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &pyListOfX509Objects)) 83 | { 84 | return NULL; 85 | } 86 | // Extract each x509 python object from the list 87 | certsCount = PyList_Size(pyListOfX509Objects); 88 | for (i=0; ix509); 97 | } 98 | return parsedCertificates; 99 | } 100 | 101 | 102 | static PyObject* nassl_X509_STORE_CTX_set0_trusted_stack(nassl_X509_STORE_CTX_Object *self, PyObject *args) 103 | { 104 | STACK_OF(X509) *trustedCerts = NULL; 105 | if (self->trustedCertificates != NULL) 106 | { 107 | PyErr_SetString(PyExc_ValueError, "set0_trusted_stack() has already been called."); 108 | return NULL; 109 | } 110 | 111 | trustedCerts = parseCertificateList(args); 112 | if (trustedCerts == NULL) 113 | { 114 | return NULL; 115 | } 116 | 117 | // Increase the OpenSSL ref count of each certificate in the chain; it get decreased in nassl_X509_STORE_CTX_dealloc() 118 | self->trustedCertificates = X509_chain_up_ref(trustedCerts); 119 | 120 | X509_STORE_CTX_set0_trusted_stack(self->x509storeCtx, trustedCerts); 121 | Py_RETURN_NONE; 122 | } 123 | 124 | 125 | static PyObject* nassl_X509_STORE_CTX_set0_untrusted(nassl_X509_STORE_CTX_Object *self, PyObject *args) 126 | { 127 | STACK_OF(X509) *untrustedCerts = NULL; 128 | if (self->untrustedCertificates != NULL) 129 | { 130 | PyErr_SetString(PyExc_ValueError, "set0_untrusted() has already been called."); 131 | return NULL; 132 | } 133 | 134 | untrustedCerts = parseCertificateList(args); 135 | if (untrustedCerts == NULL) 136 | { 137 | return NULL; 138 | } 139 | 140 | // Increase the OpenSSL ref count of each certificate in the chain; it get decreased in nassl_X509_STORE_CTX_dealloc() 141 | self->untrustedCertificates = X509_chain_up_ref(untrustedCerts); 142 | 143 | X509_STORE_CTX_set0_untrusted(self->x509storeCtx, untrustedCerts); 144 | Py_RETURN_NONE; 145 | } 146 | 147 | 148 | static PyObject* nassl_X509_STORE_CTX_set_cert(nassl_X509_STORE_CTX_Object *self, PyObject *args) 149 | { 150 | nassl_X509_Object* x509Object; 151 | if (self->leafCertificate != NULL) 152 | { 153 | PyErr_SetString(PyExc_ValueError, "set_cert() has already been called."); 154 | return NULL; 155 | } 156 | 157 | if (!PyArg_ParseTuple(args, "O!", &nassl_X509_Type, &x509Object)) 158 | { 159 | return NULL; 160 | } 161 | // Increase the OpenSSL ref count of the cert; it get decreased in nassl_X509_STORE_CTX_dealloc() 162 | X509_up_ref(x509Object->x509); 163 | self->leafCertificate = x509Object->x509; 164 | 165 | X509_STORE_CTX_set_cert(self->x509storeCtx, x509Object->x509); 166 | Py_RETURN_NONE; 167 | } 168 | 169 | 170 | static PyObject* nassl_X509_STORE_CTX_get_error(nassl_X509_STORE_CTX_Object *self, PyObject *args) 171 | { 172 | int errorValue = X509_STORE_CTX_get_error(self->x509storeCtx); 173 | return Py_BuildValue("i", errorValue); 174 | } 175 | 176 | 177 | static PyObject* nassl_X509_STORE_CTX_get1_chain(nassl_X509_STORE_CTX_Object *self, PyObject *args) 178 | { 179 | STACK_OF(X509) *verifiedCertChain = NULL; 180 | PyObject* certChainPyList = NULL; 181 | 182 | verifiedCertChain = X509_STORE_CTX_get1_chain(self->x509storeCtx); // NOT automatically freed 183 | if (verifiedCertChain == NULL) 184 | { 185 | PyErr_SetString(PyExc_ValueError, "Error getting the verified certificate chain."); 186 | return NULL; 187 | } 188 | 189 | // We'll return a Python list containing each certificate 190 | certChainPyList = stackOfX509ToPyList(verifiedCertChain); 191 | 192 | // Manually free the chain returned by get1_chain() 193 | sk_X509_pop_free(verifiedCertChain, X509_free); 194 | 195 | if (certChainPyList == NULL) 196 | { 197 | return NULL; 198 | } 199 | return certChainPyList; 200 | } 201 | 202 | 203 | static PyMethodDef nassl_X509_STORE_CTX_Object_methods[] = 204 | { 205 | {"set0_trusted_stack", (PyCFunction)nassl_X509_STORE_CTX_set0_trusted_stack, METH_VARARGS, 206 | "OpenSSL's X509_STORE_CTX_set0_trusted_stack()." 207 | }, 208 | {"set0_untrusted", (PyCFunction)nassl_X509_STORE_CTX_set0_untrusted, METH_VARARGS, 209 | "OpenSSL's X509_STORE_CTX_set0_untrusted()." 210 | }, 211 | {"set_cert", (PyCFunction)nassl_X509_STORE_CTX_set_cert, METH_VARARGS, 212 | "OpenSSL's 509_STORE_CTX_set_cert()." 213 | }, 214 | {"get_error", (PyCFunction)nassl_X509_STORE_CTX_get_error, METH_NOARGS, 215 | "OpenSSL's X509_STORE_CTX_get_error()." 216 | }, 217 | {"get1_chain", (PyCFunction)nassl_X509_STORE_CTX_get1_chain, METH_NOARGS, 218 | "OpenSSL's X509_STORE_CTX_get1_chain()." 219 | }, 220 | {NULL} // Sentinel 221 | }; 222 | 223 | 224 | PyTypeObject nassl_X509_STORE_CTX_Type = 225 | { 226 | PyVarObject_HEAD_INIT(NULL, 0) 227 | "_nassl.X509_STORE_CTX", /*tp_name*/ 228 | sizeof(nassl_X509_STORE_CTX_Object), /*tp_basicsize*/ 229 | 0, /*tp_itemsize*/ 230 | (destructor)nassl_X509_STORE_CTX_dealloc, /*tp_dealloc*/ 231 | 0, /*tp_print*/ 232 | 0, /*tp_getattr*/ 233 | 0, /*tp_setattr*/ 234 | 0, /*tp_compare*/ 235 | 0, /*tp_repr*/ 236 | 0, /*tp_as_number*/ 237 | 0, /*tp_as_sequence*/ 238 | 0, /*tp_as_mapping*/ 239 | 0, /*tp_hash */ 240 | 0, /*tp_call*/ 241 | 0, /*tp_str*/ 242 | 0, /*tp_getattro*/ 243 | 0, /*tp_setattro*/ 244 | 0, /*tp_as_buffer*/ 245 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 246 | "X509_STORE_CTX objects", /* tp_doc */ 247 | 0, /* tp_traverse */ 248 | 0, /* tp_clear */ 249 | 0, /* tp_richcompare */ 250 | 0, /* tp_weaklistoffset */ 251 | 0, /* tp_iter */ 252 | 0, /* tp_iternext */ 253 | nassl_X509_STORE_CTX_Object_methods, /* tp_methods */ 254 | 0, /* tp_members */ 255 | 0, /* tp_getset */ 256 | 0, /* tp_base */ 257 | 0, /* tp_dict */ 258 | 0, /* tp_descr_get */ 259 | 0, /* tp_descr_set */ 260 | 0, /* tp_dictoffset */ 261 | 0, /* tp_init */ 262 | 0, /* tp_alloc */ 263 | nassl_X509_STORE_CTX_new, /* tp_new */ 264 | }; 265 | 266 | 267 | void module_add_X509_STORE_CTX(PyObject* m) 268 | { 269 | nassl_X509_STORE_CTX_Type.tp_new = nassl_X509_STORE_CTX_new; 270 | if (PyType_Ready(&nassl_X509_STORE_CTX_Type) < 0) 271 | { 272 | return; 273 | } 274 | 275 | Py_INCREF(&nassl_X509_STORE_CTX_Type); 276 | PyModule_AddObject(m, "X509_STORE_CTX", (PyObject *)&nassl_X509_STORE_CTX_Type); 277 | } 278 | -------------------------------------------------------------------------------- /nassl/_nassl/nassl_SSL_CTX.c: -------------------------------------------------------------------------------- 1 | #define PY_SSIZE_T_CLEAN 2 | #include 3 | 4 | #include 5 | 6 | #include "nassl_errors.h" 7 | #include "nassl_SSL_CTX.h" 8 | #include "python_utils.h" 9 | 10 | 11 | typedef enum 12 | { 13 | sslv23, 14 | sslv2, 15 | sslv3, 16 | tlsv1, 17 | tlsv1_1, 18 | tlsv1_2, 19 | tlsv1_3 20 | } SslProtocolVersion; 21 | 22 | 23 | static int client_cert_cb(SSL *ssl, X509 **x509, EVP_PKEY **pkey) 24 | { 25 | // This callback is here so we can detect when the server wants a client cert 26 | // It will trigger an SSL_ERROR_WANT_X509_LOOKUP error during the handshake 27 | // if the server expected a client certificate and we didn't provide one 28 | return -1; 29 | } 30 | 31 | 32 | // nassl.SSL_CTX.new() 33 | static PyObject* nassl_SSL_CTX_new(PyTypeObject *type, PyObject *args, PyObject *kwds) 34 | { 35 | nassl_SSL_CTX_Object *self; 36 | int sslVersion; 37 | SSL_CTX *sslCtx; 38 | 39 | self = (nassl_SSL_CTX_Object *)type->tp_alloc(type, 0); 40 | if (self == NULL) 41 | { 42 | return NULL; 43 | } 44 | 45 | self->sslCtx = NULL; 46 | self->pkeyPasswordBuf = NULL; 47 | 48 | if (!PyArg_ParseTuple(args, "I", &sslVersion)) 49 | { 50 | Py_DECREF(self); 51 | return NULL; 52 | } 53 | 54 | switch (sslVersion) 55 | { 56 | case sslv23: 57 | sslCtx = SSL_CTX_new(SSLv23_method()); 58 | break; 59 | case sslv2: 60 | #ifdef LEGACY_OPENSSL 61 | sslCtx = SSL_CTX_new(SSLv2_method()); 62 | break; 63 | #else 64 | PyErr_SetString(PyExc_NotImplementedError, "SSL 2.0 is disabled; re-compile with -DLEGACY_OPENSSL"); 65 | Py_DECREF(self); 66 | return NULL; 67 | #endif 68 | case sslv3: 69 | #ifdef LEGACY_OPENSSL 70 | sslCtx = SSL_CTX_new(SSLv3_method()); 71 | break; 72 | #else 73 | PyErr_SetString(PyExc_NotImplementedError, "SSL 3.0 is disabled; re-compile with -DLEGACY_OPENSSL"); 74 | Py_DECREF(self); 75 | return NULL; 76 | #endif 77 | case tlsv1: 78 | sslCtx = SSL_CTX_new(TLSv1_method()); 79 | break; 80 | case tlsv1_1: 81 | sslCtx = SSL_CTX_new(TLSv1_1_method()); 82 | break; 83 | case tlsv1_2: 84 | sslCtx = SSL_CTX_new(TLSv1_2_method()); 85 | break; 86 | #ifndef LEGACY_OPENSSL 87 | case tlsv1_3: 88 | // Replicate the pre-1.1.0 OpenSSL API to avoid breaking _nassl's API 89 | // TODO(AD): Break modern _nassl's API to make it nicer by exposing min/max_proto_version 90 | sslCtx = SSL_CTX_new(TLS_client_method()); 91 | // Force TLS 1.3 92 | SSL_CTX_set_min_proto_version(sslCtx, TLS1_3_VERSION); 93 | SSL_CTX_set_max_proto_version(sslCtx, 0); 94 | break; 95 | #endif 96 | default: 97 | PyErr_SetString(PyExc_ValueError, "Invalid value for ssl version"); 98 | Py_DECREF(self); 99 | return NULL; 100 | } 101 | if (sslCtx == NULL) 102 | { 103 | raise_OpenSSL_error(); 104 | Py_DECREF(self); 105 | return NULL; 106 | } 107 | 108 | // Set the default client certificate callback 109 | SSL_CTX_set_client_cert_cb(sslCtx, client_cert_cb); 110 | 111 | self->sslCtx = sslCtx; 112 | return (PyObject *)self; 113 | } 114 | 115 | 116 | static void nassl_SSL_CTX_dealloc(nassl_SSL_CTX_Object *self) 117 | { 118 | if (self->sslCtx != NULL) 119 | { 120 | SSL_CTX_free(self->sslCtx); 121 | self->sslCtx = NULL; 122 | } 123 | 124 | if (self->pkeyPasswordBuf != NULL) 125 | { 126 | PyMem_Free(self->pkeyPasswordBuf); 127 | self->pkeyPasswordBuf = NULL; 128 | } 129 | 130 | Py_TYPE(self)->tp_free((PyObject*)self); 131 | } 132 | 133 | 134 | static PyObject* nassl_SSL_CTX_set_verify(nassl_SSL_CTX_Object *self, PyObject *args) 135 | { 136 | int verifyMode; 137 | if (!PyArg_ParseTuple(args, "I", &verifyMode)) 138 | { 139 | return NULL; 140 | } 141 | 142 | switch (verifyMode) 143 | { 144 | case SSL_VERIFY_NONE: 145 | case SSL_VERIFY_PEER: 146 | case SSL_VERIFY_FAIL_IF_NO_PEER_CERT: 147 | case SSL_VERIFY_CLIENT_ONCE: 148 | SSL_CTX_set_verify(self->sslCtx, verifyMode, NULL); 149 | break; 150 | default: 151 | PyErr_SetString(PyExc_ValueError, "Invalid value for verification mode"); 152 | return NULL; 153 | } 154 | 155 | Py_RETURN_NONE; 156 | } 157 | 158 | 159 | static PyObject* nassl_SSL_CTX_load_verify_locations(nassl_SSL_CTX_Object *self, PyObject *args) 160 | { 161 | char *caFilePath = NULL; 162 | if (PyArg_ParseFilePath(args, &caFilePath) == NULL) 163 | { 164 | return NULL; 165 | } 166 | 167 | if (!SSL_CTX_load_verify_locations(self->sslCtx, caFilePath, NULL)) 168 | { 169 | return raise_OpenSSL_error(); 170 | } 171 | 172 | Py_RETURN_NONE; 173 | } 174 | 175 | 176 | static PyObject* nassl_SSL_CTX_use_certificate_chain_file(nassl_SSL_CTX_Object *self, PyObject *args) 177 | { 178 | char *filePath = NULL; 179 | if (PyArg_ParseFilePath(args, &filePath) == NULL) 180 | { 181 | return NULL; 182 | } 183 | 184 | if (SSL_CTX_use_certificate_chain_file(self->sslCtx, filePath) != 1 ){ 185 | return raise_OpenSSL_error(); 186 | } 187 | 188 | Py_RETURN_NONE; 189 | } 190 | 191 | 192 | static PyObject* nassl_SSL_CTX_use_PrivateKey_file(nassl_SSL_CTX_Object *self, PyObject *args) 193 | { 194 | char *filePath = NULL; 195 | int certType = 0; 196 | PyObject *pyFilePath = NULL; 197 | if (!PyArg_ParseTuple(args, "O&I", PyUnicode_FSConverter, &pyFilePath, &certType)) 198 | { 199 | return NULL; 200 | } 201 | filePath = PyBytes_AsString(pyFilePath); // Must not be deallocated 202 | if (filePath == NULL) 203 | { 204 | PyErr_SetString(PyExc_ValueError, "Could not extract the file path"); 205 | return NULL; 206 | } 207 | 208 | if (SSL_CTX_use_PrivateKey_file(self->sslCtx, filePath, certType) != 1) 209 | { 210 | return raise_OpenSSL_error(); 211 | } 212 | 213 | Py_RETURN_NONE; 214 | } 215 | 216 | 217 | static PyObject* nassl_SSL_CTX_check_private_key(nassl_SSL_CTX_Object *self, PyObject *args) 218 | { 219 | if (SSL_CTX_check_private_key(self->sslCtx) != 1) 220 | { 221 | return raise_OpenSSL_error(); 222 | } 223 | 224 | Py_RETURN_NONE; 225 | } 226 | 227 | 228 | // passwd callback for encrypted PEM file handling 229 | static int pem_passwd_cb(char *buf, int size, int rwflag, void *userdata) 230 | { 231 | // This is a hack to allow callers to provide the password to unlock 232 | // a PEM private key whenever they want instead of when the SSL_CTX 233 | // object gets created (which would be less hacky and convenient) 234 | // The pointer to the buffer containing the user's password is at userdata 235 | size_t passwordSize = 0; 236 | char *passwordBuf = (char *)userdata; 237 | 238 | if ((userdata == NULL) || (buf == NULL)) 239 | { 240 | return 0; 241 | } 242 | 243 | // NUL-terminated string as it will come from Python 244 | passwordSize = strlen(passwordBuf) + 1; 245 | if (passwordSize > (unsigned) size) 246 | { 247 | // Not enough space in OpenSSL's buffer 248 | return 0; 249 | } 250 | 251 | strncpy(buf, passwordBuf, passwordSize); 252 | // OpenSSL wants the size of the password 253 | return (int) strlen(passwordBuf); 254 | } 255 | 256 | 257 | static PyObject* nassl_SSL_CTX_set_private_key_password(nassl_SSL_CTX_Object *self, PyObject *args) 258 | { 259 | size_t passwordSize = 0; 260 | char *passwordStr = NULL; 261 | if (!PyArg_ParseTuple(args, "s", &passwordStr)) 262 | { 263 | return NULL; 264 | } 265 | 266 | // Store the password; Python gives us a NUL-terminated string 267 | passwordSize = strlen(passwordStr) + 1; 268 | self->pkeyPasswordBuf = (char *) PyMem_Malloc(passwordSize); 269 | if (self->pkeyPasswordBuf == NULL) 270 | { 271 | return PyErr_NoMemory(); 272 | } 273 | 274 | strncpy(self->pkeyPasswordBuf, passwordStr, passwordSize); 275 | 276 | // Set up the OpenSSL callbacks 277 | SSL_CTX_set_default_passwd_cb(self->sslCtx, &pem_passwd_cb); 278 | SSL_CTX_set_default_passwd_cb_userdata(self->sslCtx, self->pkeyPasswordBuf); 279 | 280 | Py_RETURN_NONE; 281 | } 282 | 283 | static PyObject* nassl_SSL_CTX_set_client_cert_cb_NULL(nassl_SSL_CTX_Object *self, PyObject *args) 284 | { 285 | SSL_CTX_set_client_cert_cb(self->sslCtx, NULL); 286 | Py_RETURN_NONE; 287 | } 288 | 289 | 290 | static PyMethodDef nassl_SSL_CTX_Object_methods[] = 291 | { 292 | {"set_verify", (PyCFunction)nassl_SSL_CTX_set_verify, METH_VARARGS, 293 | "OpenSSL's SSL_CTX_set_verify() with a NULL verify_callback." 294 | }, 295 | {"load_verify_locations", (PyCFunction)nassl_SSL_CTX_load_verify_locations, METH_VARARGS, 296 | "OpenSSL's SSL_CTX_load_verify_locations() with a NULL CAPath." 297 | }, 298 | {"use_certificate_chain_file", (PyCFunction)nassl_SSL_CTX_use_certificate_chain_file, METH_VARARGS, 299 | "OpenSSL's SSL_CTX_use_certificate_chain_file()." 300 | }, 301 | {"use_PrivateKey_file", (PyCFunction)nassl_SSL_CTX_use_PrivateKey_file, METH_VARARGS, 302 | "OpenSSL's SSL_CTX_use_PrivateKey_file()." 303 | }, 304 | {"check_private_key", (PyCFunction)nassl_SSL_CTX_check_private_key, METH_NOARGS, 305 | "OpenSSL's SSL_CTX_check_private_key()." 306 | }, 307 | {"set_private_key_password", (PyCFunction)nassl_SSL_CTX_set_private_key_password, METH_VARARGS, 308 | "Sets up a default callback for encrypted PEM file handling using OpenSSL's SSL_CTX_set_default_passwd_cb() with a hardcoded callback, and then stores the supplied password to be used for subsequent PEM decryption operations." 309 | }, 310 | {"set_client_cert_cb_NULL", (PyCFunction)nassl_SSL_CTX_set_client_cert_cb_NULL, METH_NOARGS, 311 | "Configure a NULL client certificate callback in order to ignore client certificate requests from the server and continue even if no certificate was provided." 312 | }, 313 | {NULL} // Sentinel 314 | }; 315 | /* 316 | 317 | static PyMemberDef nassl_SSL_CTX_Object_members[] = { 318 | {NULL} // Sentinel 319 | }; 320 | */ 321 | 322 | PyTypeObject nassl_SSL_CTX_Type = 323 | { 324 | PyVarObject_HEAD_INIT(NULL, 0) 325 | "_nassl.SSL_CTX", /*tp_name*/ 326 | sizeof(nassl_SSL_CTX_Object), /*tp_basicsize*/ 327 | 0, /*tp_itemsize*/ 328 | (destructor)nassl_SSL_CTX_dealloc, /*tp_dealloc*/ 329 | 0, /*tp_print*/ 330 | 0, /*tp_getattr*/ 331 | 0, /*tp_setattr*/ 332 | 0, /*tp_compare*/ 333 | 0, /*tp_repr*/ 334 | 0, /*tp_as_number*/ 335 | 0, /*tp_as_sequence*/ 336 | 0, /*tp_as_mapping*/ 337 | 0, /*tp_hash */ 338 | 0, /*tp_call*/ 339 | 0, /*tp_str*/ 340 | 0, /*tp_getattro*/ 341 | 0, /*tp_setattro*/ 342 | 0, /*tp_as_buffer*/ 343 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 344 | "SSL_CTX objects", /* tp_doc */ 345 | 0, /* tp_traverse */ 346 | 0, /* tp_clear */ 347 | 0, /* tp_richcompare */ 348 | 0, /* tp_weaklistoffset */ 349 | 0, /* tp_iter */ 350 | 0, /* tp_iternext */ 351 | nassl_SSL_CTX_Object_methods, /* tp_methods */ 352 | 0, /* tp_members */ 353 | 0, /* tp_getset */ 354 | 0, /* tp_base */ 355 | 0, /* tp_dict */ 356 | 0, /* tp_descr_get */ 357 | 0, /* tp_descr_set */ 358 | 0, /* tp_dictoffset */ 359 | 0, /* tp_init */ 360 | 0, /* tp_alloc */ 361 | nassl_SSL_CTX_new, /* tp_new */ 362 | }; 363 | 364 | 365 | 366 | void module_add_SSL_CTX(PyObject* m) 367 | { 368 | nassl_SSL_CTX_Type.tp_new = nassl_SSL_CTX_new; 369 | if (PyType_Ready(&nassl_SSL_CTX_Type) < 0) 370 | { 371 | return; 372 | } 373 | 374 | Py_INCREF(&nassl_SSL_CTX_Type); 375 | PyModule_AddObject(m, "SSL_CTX", (PyObject *)&nassl_SSL_CTX_Type); 376 | } 377 | 378 | -------------------------------------------------------------------------------- /tests/SSL_CTX_test.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | 3 | import pytest 4 | 5 | from nassl import _nassl, _nassl_legacy 6 | from nassl.ssl_client import OpenSslVersionEnum, OpenSslVerifyEnum, OpenSslFileTypeEnum 7 | 8 | 9 | @pytest.mark.parametrize("nassl_module", [_nassl, _nassl_legacy]) 10 | class TestCommonSSL_CTX: 11 | def test_new(self, nassl_module): 12 | assert nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 13 | 14 | def test_new_bad(self, nassl_module): 15 | # Invalid protocol constant 16 | with pytest.raises(ValueError): 17 | nassl_module.SSL_CTX(1234) 18 | 19 | def test_set_verify(self, nassl_module): 20 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 21 | test_ssl_ctx.set_verify(OpenSslVerifyEnum.PEER.value) 22 | 23 | def test_set_verify_bad(self, nassl_module): 24 | # Invalid verify constant 25 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 26 | with pytest.raises(ValueError): 27 | test_ssl_ctx.set_verify(1235) 28 | 29 | def test_load_verify_locations(self, nassl_module): 30 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 31 | test_file = tempfile.NamedTemporaryFile(delete=False, mode="wt") 32 | test_file.write( 33 | """-----BEGIN CERTIFICATE----- 34 | MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV 35 | UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy 36 | dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 37 | MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx 38 | dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B 39 | AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f 40 | BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A 41 | cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC 42 | AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ 43 | MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm 44 | aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw 45 | ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj 46 | IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF 47 | MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA 48 | A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y 49 | 7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh 50 | 1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 51 | -----END CERTIFICATE-----""" 52 | ) 53 | test_file.close() 54 | test_ssl_ctx.load_verify_locations(test_file.name) 55 | 56 | def test_load_verify_locations_bad(self, nassl_module): 57 | # Certificate file doesn't exist 58 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 59 | with pytest.raises(_nassl.OpenSSLError): 60 | test_ssl_ctx.load_verify_locations("tests") 61 | 62 | def test_set_private_key_password_null_byte(self, nassl_module): 63 | # NULL byte embedded in the password 64 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 65 | # It raises a TypeError on Python 2.7 and 3.4, and a ValueError on 3.5 66 | with pytest.raises(Exception, match=" null"): 67 | test_ssl_ctx.set_private_key_password("AAA\x00AAAA") 68 | 69 | def test_use_certificate_file(self, nassl_module): 70 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 71 | test_file = tempfile.NamedTemporaryFile(delete=False, mode="wt") 72 | test_file.write( 73 | """-----BEGIN CERTIFICATE----- 74 | MIIDCjCCAnOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBgDELMAkGA1UEBhMCRlIx 75 | DjAMBgNVBAgMBVBhcmlzMQ4wDAYDVQQHDAVQYXJpczEWMBQGA1UECgwNRGFzdGFy 76 | ZGx5IEluYzEMMAoGA1UECwwDMTIzMQ8wDQYDVQQDDAZBbCBCYW4xGjAYBgkqhkiG 77 | 9w0BCQEWC2xvbEBsb2wuY29tMB4XDTEzMDEyNzAwMDM1OFoXDTE0MDEyNzAwMDM1 78 | OFowgZcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDAMxMjMxDTALBgNVBAcMBFRlc3Qx 79 | IjAgBgNVBAoMGUludHJvc3B5IFRlc3QgQ2xpZW50IENlcnQxCzAJBgNVBAsMAjEy 80 | MRUwEwYDVQQDDAxBbGJhbiBEaXF1ZXQxIzAhBgkqhkiG9w0BCQEWFG5hYmxhLWMw 81 | ZDNAZ21haWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlnvP1ltVO 82 | 8JDNT3AA99QqtiqCi/7BeEcFDm2al46mv7looz6CmB84osrusNVFsS5ICLbrCmeo 83 | w5sxW7VVveGueBQyWynngl2PmmufA5Mhwq0ZY8CvwV+O7m0hEXxzwbyGa23ai16O 84 | zIiaNlBAb0mC2vwJbsc3MTMovE6dHUgmzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG 85 | CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV 86 | HQ4EFgQUYR45okpFsqTYB1wlQQblLH9cRdgwHwYDVR0jBBgwFoAUP0X2HQlaca7D 87 | NBzVbsjsdhzOqUQwDQYJKoZIhvcNAQEFBQADgYEAWEOxpRjvKvTurDXK/sEUw2KY 88 | gmbbGP3tF+fQ/6JS1VdCdtLxxJAHHTW62ugVTlmJZtpsEGlg49BXAEMblLY/K7nm 89 | dWN8oZL+754GaBlJ+wK6/Nz4YcuByJAnN8OeTY4Acxjhks8PrAbZgcf0FdpJaAlk 90 | Pd2eQ9+DkopOz3UGU7c= 91 | -----END CERTIFICATE----- 92 | -----BEGIN CERTIFICATE----- 93 | MIIDCjCCAnOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBgDELMAkGA1UEBhMCRlIx 94 | DjAMBgNVBAgMBVBhcmlzMQ4wDAYDVQQHDAVQYXJpczEWMBQGA1UECgwNRGFzdGFy 95 | ZGx5IEluYzEMMAoGA1UECwwDMTIzMQ8wDQYDVQQDDAZBbCBCYW4xGjAYBgkqhkiG 96 | 9w0BCQEWC2xvbEBsb2wuY29tMB4XDTEzMDEyNzAwMDM1OFoXDTE0MDEyNzAwMDM1 97 | OFowgZcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDAMxMjMxDTALBgNVBAcMBFRlc3Qx 98 | IjAgBgNVBAoMGUludHJvc3B5IFRlc3QgQ2xpZW50IENlcnQxCzAJBgNVBAsMAjEy 99 | MRUwEwYDVQQDDAxBbGJhbiBEaXF1ZXQxIzAhBgkqhkiG9w0BCQEWFG5hYmxhLWMw 100 | ZDNAZ21haWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlnvP1ltVO 101 | 8JDNT3AA99QqtiqCi/7BeEcFDm2al46mv7looz6CmB84osrusNVFsS5ICLbrCmeo 102 | w5sxW7VVveGueBQyWynngl2PmmufA5Mhwq0ZY8CvwV+O7m0hEXxzwbyGa23ai16O 103 | zIiaNlBAb0mC2vwJbsc3MTMovE6dHUgmzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG 104 | CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV 105 | HQ4EFgQUYR45okpFsqTYB1wlQQblLH9cRdgwHwYDVR0jBBgwFoAUP0X2HQlaca7D 106 | NBzVbsjsdhzOqUQwDQYJKoZIhvcNAQEFBQADgYEAWEOxpRjvKvTurDXK/sEUw2KY 107 | gmbbGP3tF+fQ/6JS1VdCdtLxxJAHHTW62ugVTlmJZtpsEGlg49BXAEMblLY/K7nm 108 | dWN8oZL+754GaBlJ+wK6/Nz4YcuByJAnN8OeTY4Acxjhks8PrAbZgcf0FdpJaAlk 109 | Pd2eQ9+DkopOz3UGU7c= 110 | -----END CERTIFICATE-----""" 111 | ) 112 | test_file.close() 113 | test_ssl_ctx.use_certificate_chain_file(test_file.name) 114 | 115 | def test_use_certificate_file_bad(self, nassl_module): 116 | # Bad filename 117 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 118 | with pytest.raises(_nassl.OpenSSLError, match="system lib"): 119 | test_ssl_ctx.use_certificate_chain_file("invalidPath") 120 | 121 | def test_use_PrivateKey_file(self, nassl_module): 122 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 123 | test_file = tempfile.NamedTemporaryFile(delete=False, mode="wt") 124 | test_file.write( 125 | """-----BEGIN PRIVATE KEY----- 126 | MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOWe8/WW1U7wkM1P 127 | cAD31Cq2KoKL/sF4RwUObZqXjqa/uWijPoKYHziiyu6w1UWxLkgItusKZ6jDmzFb 128 | tVW94a54FDJbKeeCXY+aa58DkyHCrRljwK/BX47ubSERfHPBvIZrbdqLXo7MiJo2 129 | UEBvSYLa/AluxzcxMyi8Tp0dSCbNAgMBAAECgYAl0ZpItsEHMWQIDK9b2XWeW0aB 130 | HeGlp9O6p3ex4IhkOmulKk3fYIKz50wZKBLYWahPwO+vopUUHLNw27PwHUgQDmOY 131 | QKAZowO3X5RT5URNzeiI2KTE431uNFqeMR9+XrnjQIZPDDaltACTTZpFp1rFqM+C 132 | /WbZ2VHS/52Vrrj7wQJBAPW64ts+UHNQn1Y+CyYQGVERICdPwC4nSu/+MYpvo0r+ 133 | XX1bali8kTdBs2ByoWQOaFr3B4qffd4vb8lIMxt6f3kCQQDvN7ZUsyM/HcSw/4go 134 | pGakZx1OJKBCet6uNA6ymglhDzmFoiAR3QAIxYTVQlc87m0v4ExjVC/nlbdNa4MX 135 | m2j1AkAHgagAbozimOnlJowMo51CXrWOvd7vCgA+CJPW2MYyOkb811gOUeRVvcoO 136 | /jFz7wS9EqLGV0zvBp/xlCULh9hxAkEA2x+tZOiy4J3kDj4D+zaczvulXG8wXbUv 137 | RWNqEzAGZ2IKzt4zgiluXpqPksmyH55HZhOP5Wy4dOovfjt9WaKCAQJAEzgPLx+6 138 | iuiRanrS8dy8Q5UXavmPgBeHXZ4gxWbXD3vC5Qzorgp+P04GhofSCFklXokTPrKN 139 | jsXbhxAIkrdmpg== 140 | -----END PRIVATE KEY-----""" 141 | ) 142 | test_file.close() 143 | test_ssl_ctx.use_PrivateKey_file(test_file.name, OpenSslFileTypeEnum.PEM.value) 144 | 145 | def test_use_PrivateKey_file_bad(self, nassl_module): 146 | # Bad filename 147 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 148 | with pytest.raises(_nassl.OpenSSLError, match="No such file"): 149 | test_ssl_ctx.use_PrivateKey_file("invalidPath", OpenSslFileTypeEnum.PEM.value) 150 | 151 | def test_check_private_key(self, nassl_module): 152 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 153 | test_file = tempfile.NamedTemporaryFile(delete=False, mode="wt") 154 | test_file.write( 155 | """-----BEGIN PRIVATE KEY----- 156 | MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOWe8/WW1U7wkM1P 157 | cAD31Cq2KoKL/sF4RwUObZqXjqa/uWijPoKYHziiyu6w1UWxLkgItusKZ6jDmzFb 158 | tVW94a54FDJbKeeCXY+aa58DkyHCrRljwK/BX47ubSERfHPBvIZrbdqLXo7MiJo2 159 | UEBvSYLa/AluxzcxMyi8Tp0dSCbNAgMBAAECgYAl0ZpItsEHMWQIDK9b2XWeW0aB 160 | HeGlp9O6p3ex4IhkOmulKk3fYIKz50wZKBLYWahPwO+vopUUHLNw27PwHUgQDmOY 161 | QKAZowO3X5RT5URNzeiI2KTE431uNFqeMR9+XrnjQIZPDDaltACTTZpFp1rFqM+C 162 | /WbZ2VHS/52Vrrj7wQJBAPW64ts+UHNQn1Y+CyYQGVERICdPwC4nSu/+MYpvo0r+ 163 | XX1bali8kTdBs2ByoWQOaFr3B4qffd4vb8lIMxt6f3kCQQDvN7ZUsyM/HcSw/4go 164 | pGakZx1OJKBCet6uNA6ymglhDzmFoiAR3QAIxYTVQlc87m0v4ExjVC/nlbdNa4MX 165 | m2j1AkAHgagAbozimOnlJowMo51CXrWOvd7vCgA+CJPW2MYyOkb811gOUeRVvcoO 166 | /jFz7wS9EqLGV0zvBp/xlCULh9hxAkEA2x+tZOiy4J3kDj4D+zaczvulXG8wXbUv 167 | RWNqEzAGZ2IKzt4zgiluXpqPksmyH55HZhOP5Wy4dOovfjt9WaKCAQJAEzgPLx+6 168 | iuiRanrS8dy8Q5UXavmPgBeHXZ4gxWbXD3vC5Qzorgp+P04GhofSCFklXokTPrKN 169 | jsXbhxAIkrdmpg== 170 | -----END PRIVATE KEY-----""" 171 | ) 172 | test_file.close() 173 | test_file2 = tempfile.NamedTemporaryFile(delete=False, mode="wt") 174 | test_file2.write( 175 | """-----BEGIN CERTIFICATE----- 176 | MIIDCjCCAnOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBgDELMAkGA1UEBhMCRlIx 177 | DjAMBgNVBAgMBVBhcmlzMQ4wDAYDVQQHDAVQYXJpczEWMBQGA1UECgwNRGFzdGFy 178 | ZGx5IEluYzEMMAoGA1UECwwDMTIzMQ8wDQYDVQQDDAZBbCBCYW4xGjAYBgkqhkiG 179 | 9w0BCQEWC2xvbEBsb2wuY29tMB4XDTEzMDEyNzAwMDM1OFoXDTE0MDEyNzAwMDM1 180 | OFowgZcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDAMxMjMxDTALBgNVBAcMBFRlc3Qx 181 | IjAgBgNVBAoMGUludHJvc3B5IFRlc3QgQ2xpZW50IENlcnQxCzAJBgNVBAsMAjEy 182 | MRUwEwYDVQQDDAxBbGJhbiBEaXF1ZXQxIzAhBgkqhkiG9w0BCQEWFG5hYmxhLWMw 183 | ZDNAZ21haWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlnvP1ltVO 184 | 8JDNT3AA99QqtiqCi/7BeEcFDm2al46mv7looz6CmB84osrusNVFsS5ICLbrCmeo 185 | w5sxW7VVveGueBQyWynngl2PmmufA5Mhwq0ZY8CvwV+O7m0hEXxzwbyGa23ai16O 186 | zIiaNlBAb0mC2vwJbsc3MTMovE6dHUgmzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG 187 | CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV 188 | HQ4EFgQUYR45okpFsqTYB1wlQQblLH9cRdgwHwYDVR0jBBgwFoAUP0X2HQlaca7D 189 | NBzVbsjsdhzOqUQwDQYJKoZIhvcNAQEFBQADgYEAWEOxpRjvKvTurDXK/sEUw2KY 190 | gmbbGP3tF+fQ/6JS1VdCdtLxxJAHHTW62ugVTlmJZtpsEGlg49BXAEMblLY/K7nm 191 | dWN8oZL+754GaBlJ+wK6/Nz4YcuByJAnN8OeTY4Acxjhks8PrAbZgcf0FdpJaAlk 192 | Pd2eQ9+DkopOz3UGU7c= 193 | -----END CERTIFICATE----- 194 | -----BEGIN CERTIFICATE----- 195 | MIIDCjCCAnOgAwIBAgIBAjANBgkqhkiG9w0BAQUFADCBgDELMAkGA1UEBhMCRlIx 196 | DjAMBgNVBAgMBVBhcmlzMQ4wDAYDVQQHDAVQYXJpczEWMBQGA1UECgwNRGFzdGFy 197 | ZGx5IEluYzEMMAoGA1UECwwDMTIzMQ8wDQYDVQQDDAZBbCBCYW4xGjAYBgkqhkiG 198 | 9w0BCQEWC2xvbEBsb2wuY29tMB4XDTEzMDEyNzAwMDM1OFoXDTE0MDEyNzAwMDM1 199 | OFowgZcxCzAJBgNVBAYTAkZSMQwwCgYDVQQIDAMxMjMxDTALBgNVBAcMBFRlc3Qx 200 | IjAgBgNVBAoMGUludHJvc3B5IFRlc3QgQ2xpZW50IENlcnQxCzAJBgNVBAsMAjEy 201 | MRUwEwYDVQQDDAxBbGJhbiBEaXF1ZXQxIzAhBgkqhkiG9w0BCQEWFG5hYmxhLWMw 202 | ZDNAZ21haWwuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlnvP1ltVO 203 | 8JDNT3AA99QqtiqCi/7BeEcFDm2al46mv7looz6CmB84osrusNVFsS5ICLbrCmeo 204 | w5sxW7VVveGueBQyWynngl2PmmufA5Mhwq0ZY8CvwV+O7m0hEXxzwbyGa23ai16O 205 | zIiaNlBAb0mC2vwJbsc3MTMovE6dHUgmzQIDAQABo3sweTAJBgNVHRMEAjAAMCwG 206 | CWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNV 207 | HQ4EFgQUYR45okpFsqTYB1wlQQblLH9cRdgwHwYDVR0jBBgwFoAUP0X2HQlaca7D 208 | NBzVbsjsdhzOqUQwDQYJKoZIhvcNAQEFBQADgYEAWEOxpRjvKvTurDXK/sEUw2KY 209 | gmbbGP3tF+fQ/6JS1VdCdtLxxJAHHTW62ugVTlmJZtpsEGlg49BXAEMblLY/K7nm 210 | dWN8oZL+754GaBlJ+wK6/Nz4YcuByJAnN8OeTY4Acxjhks8PrAbZgcf0FdpJaAlk 211 | Pd2eQ9+DkopOz3UGU7c= 212 | -----END CERTIFICATE-----""" 213 | ) 214 | test_file2.close() 215 | test_ssl_ctx.use_certificate_chain_file(test_file2.name) 216 | test_ssl_ctx.use_PrivateKey_file(test_file.name, OpenSslFileTypeEnum.PEM.value) 217 | test_ssl_ctx.check_private_key() 218 | 219 | def test_check_private_key_bad(self, nassl_module): 220 | test_ssl_ctx = nassl_module.SSL_CTX(OpenSslVersionEnum.SSLV23.value) 221 | with pytest.raises(_nassl.OpenSSLError, match="no certificate assigned"): 222 | test_ssl_ctx.check_private_key() 223 | 224 | # TODO: add get_ca_list tests 225 | 226 | 227 | class TestModernSSL_CTX: 228 | def test_tlsv1_3(self): 229 | ssl_ctx = _nassl.SSL_CTX(OpenSslVersionEnum.TLSV1_3) 230 | assert ssl_ctx 231 | -------------------------------------------------------------------------------- /nassl/ssl_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from abc import ABC 3 | from pathlib import Path 4 | 5 | from nassl import _nassl 6 | from nassl._nassl import WantReadError, OpenSSLError, WantX509LookupError 7 | 8 | from enum import IntEnum 9 | from typing import List, Any, Tuple 10 | 11 | from typing import Protocol 12 | 13 | 14 | from typing import Optional 15 | from nassl.ephemeral_key_info import ( 16 | OpenSslEvpPkeyEnum, 17 | EphemeralKeyInfo, 18 | DhEphemeralKeyInfo, 19 | EcDhEphemeralKeyInfo, 20 | NistEcDhKeyExchangeInfo, 21 | OpenSslEcNidEnum, 22 | ) 23 | from nassl.cert_chain_verifier import CertificateChainVerificationFailed 24 | 25 | 26 | class OpenSslVerifyEnum(IntEnum): 27 | """SSL validation options which map to the SSL_VERIFY_XXX OpenSSL constants.""" 28 | 29 | NONE = 0 30 | PEER = 1 31 | FAIL_IF_NO_PEER_CERT = 2 32 | CLIENT_ONCE = 4 33 | 34 | 35 | class OpenSslDigestNidEnum(IntEnum): 36 | """SSL digest algorithms used for the signature algorithm, per obj_mac.h.""" 37 | 38 | MD5 = 4 39 | SHA1 = 64 40 | SHA224 = 675 41 | SHA256 = 672 42 | SHA384 = 673 43 | SHA512 = 674 44 | 45 | 46 | class OpenSslVersionEnum(IntEnum): 47 | """SSL version constants.""" 48 | 49 | SSLV23 = 0 50 | SSLV2 = 1 51 | SSLV3 = 2 52 | TLSV1 = 3 53 | TLSV1_1 = 4 54 | TLSV1_2 = 5 55 | TLSV1_3 = 6 56 | 57 | 58 | class OpenSslFileTypeEnum(IntEnum): 59 | """Certificate and private key format constants which map to the SSL_FILETYPE_XXX OpenSSL constants.""" 60 | 61 | PEM = 1 62 | ASN1 = 2 63 | 64 | 65 | class ClientCertificateRequested(Exception): 66 | ERROR_MSG_CAS = "Server requested a client certificate issued by one of the following CAs: {0}." 67 | ERROR_MSG = "Server requested a client certificate." 68 | 69 | def __init__(self, ca_list: List[str]) -> None: 70 | self._ca_list = ca_list 71 | 72 | def __str__(self) -> str: 73 | exc_msg = self.ERROR_MSG 74 | 75 | if len(self._ca_list) > 0: 76 | exc_msg = self.ERROR_MSG_CAS.format(", ".join(self._ca_list)) 77 | 78 | return exc_msg 79 | 80 | 81 | class NasslModuleProtocol(Protocol): 82 | SSL_CTX: Any 83 | SSL: Any 84 | BIO: Any 85 | X509: Any 86 | X509_STORE_CTX: Any 87 | OCSP_RESPONSE: Any 88 | OpenSSLError: Any 89 | WantReadError: Any 90 | WantX509LookupError: Any 91 | SSL_SESSION: Any 92 | 93 | 94 | class BaseSslClient(ABC): 95 | """Common code and methods to the modern and legacy SSL clients.""" 96 | 97 | _DEFAULT_BUFFER_SIZE = 4096 98 | 99 | # The version of OpenSSL/nassl to use (modern VS legacy) 100 | _NASSL_MODULE: NasslModuleProtocol 101 | 102 | def __init__( 103 | self, 104 | underlying_socket: Optional[socket.socket] = None, 105 | ssl_version: OpenSslVersionEnum = OpenSslVersionEnum.SSLV23, 106 | ssl_verify: OpenSslVerifyEnum = OpenSslVerifyEnum.PEER, 107 | ssl_verify_locations: Optional[Path] = None, 108 | client_certificate_chain: Optional[Path] = None, 109 | client_key: Optional[Path] = None, 110 | client_key_type: OpenSslFileTypeEnum = OpenSslFileTypeEnum.PEM, 111 | client_key_password: str = "", 112 | ignore_client_authentication_requests: bool = False, 113 | server_name_indication: Optional[str] = None, 114 | ) -> None: 115 | self._init_base_objects(ssl_version, underlying_socket) 116 | 117 | # Warning: Anything that modifies the SSL_CTX must be done before creating the SSL object 118 | # Otherwise changes to the SSL_CTX do not get propagated to future SSL objects 119 | self._init_server_authentication(ssl_verify, ssl_verify_locations) 120 | self._init_client_authentication( 121 | client_certificate_chain, 122 | client_key, 123 | client_key_type, 124 | client_key_password, 125 | ignore_client_authentication_requests, 126 | ) 127 | # Now create the SSL object 128 | self._init_ssl_objects() 129 | if server_name_indication is not None: 130 | self._ssl.set_tlsext_host_name(server_name_indication) 131 | 132 | def _init_base_objects( 133 | self, 134 | ssl_version: OpenSslVersionEnum, 135 | underlying_socket: Optional[socket.socket], 136 | ) -> None: 137 | """Setup the socket and SSL_CTX objects.""" 138 | self._is_handshake_completed = False 139 | self._ssl_version = ssl_version 140 | self._ssl_ctx = self._NASSL_MODULE.SSL_CTX(ssl_version.value) 141 | 142 | # A Python socket handles transmission of the data 143 | self._sock = underlying_socket 144 | 145 | def _init_server_authentication(self, ssl_verify: OpenSslVerifyEnum, ssl_verify_locations: Optional[Path]) -> None: 146 | """Setup the certificate validation logic for authenticating the server.""" 147 | self._ssl_ctx.set_verify(ssl_verify.value) 148 | if ssl_verify_locations: 149 | # Ensure the file exists 150 | with ssl_verify_locations.open(): 151 | pass 152 | self._ssl_ctx.load_verify_locations(str(ssl_verify_locations)) 153 | 154 | def _init_client_authentication( 155 | self, 156 | client_certificate_chain: Optional[Path], 157 | client_key: Optional[Path], 158 | client_key_type: OpenSslFileTypeEnum, 159 | client_key_password: str, 160 | ignore_client_authentication_requests: bool, 161 | ) -> None: 162 | """Setup client authentication using the supplied certificate and key.""" 163 | if client_certificate_chain is not None and client_key is not None: 164 | self._use_private_key( 165 | client_certificate_chain, 166 | client_key, 167 | client_key_type, 168 | client_key_password, 169 | ) 170 | 171 | if ignore_client_authentication_requests: 172 | if client_certificate_chain: 173 | raise ValueError("Cannot enable both client_certchain_file and ignore_client_authentication_requests") 174 | 175 | self._ssl_ctx.set_client_cert_cb_NULL() 176 | 177 | def _init_ssl_objects(self) -> None: 178 | self._ssl = self._NASSL_MODULE.SSL(self._ssl_ctx) 179 | self._ssl.set_connect_state() 180 | 181 | self._internal_bio = self._NASSL_MODULE.BIO() 182 | self._network_bio = self._NASSL_MODULE.BIO() 183 | 184 | # http://www.openssl.org/docs/crypto/BIO_s_bio.html 185 | self._NASSL_MODULE.BIO.make_bio_pair(self._internal_bio, self._network_bio) 186 | self._ssl.set_bio(self._internal_bio) 187 | self._ssl.set_network_bio_to_free_when_dealloc(self._network_bio) 188 | 189 | def set_underlying_socket(self, sock: socket.socket) -> None: 190 | if self._sock: 191 | raise RuntimeError("A socket was already set") 192 | self._sock = sock 193 | 194 | def get_underlying_socket(self) -> Optional[socket.socket]: 195 | return self._sock 196 | 197 | def do_handshake(self) -> None: 198 | if self._sock is None: 199 | # TODO: Auto create a socket ? 200 | raise IOError("Internal socket set to None; cannot perform handshake.") 201 | 202 | while True: 203 | try: 204 | self._ssl.do_handshake() 205 | self._is_handshake_completed = True 206 | # Handshake was successful 207 | return 208 | 209 | except WantReadError: 210 | # OpenSSL is expecting more data from the peer 211 | # Send available handshake data to the peer 212 | self._flush_ssl_engine() 213 | 214 | # Recover the peer's encrypted response 215 | handshake_data_in = self._sock.recv(self._DEFAULT_BUFFER_SIZE) 216 | if len(handshake_data_in) == 0: 217 | raise IOError("Nassl SSL handshake failed: peer did not send data back.") 218 | # Pass the data to the SSL engine 219 | self._network_bio.write(handshake_data_in) 220 | 221 | except WantX509LookupError: 222 | # Server asked for a client certificate and we didn't provide one 223 | raise ClientCertificateRequested(self.get_client_CA_list()) 224 | 225 | except OpenSSLError as e: 226 | if "alert bad certificate" in e.args[0]: 227 | # Bad certificate alert (https://github.com/nabla-c0d3/sslyze/issues/313 ) 228 | raise ClientCertificateRequested(self.get_client_CA_list()) 229 | if "sslv3 alert certificate unknown" in e.args[0]: 230 | # Some banking websites do that: https://github.com/nabla-c0d3/sslyze/issues/531 231 | raise ClientCertificateRequested(self.get_client_CA_list()) 232 | else: 233 | raise 234 | 235 | def is_handshake_completed(self) -> bool: 236 | return self._is_handshake_completed 237 | 238 | # When sending early data, client can call read even if the handshake hasn't been 239 | # finished yet 240 | def read(self, size: int, handshake_must_be_completed: bool = True) -> bytes: 241 | if self._sock is None: 242 | raise IOError("Internal socket set to None; cannot perform handshake.") 243 | if handshake_must_be_completed and not self._is_handshake_completed: 244 | raise IOError("SSL Handshake was not completed; cannot receive data.") 245 | 246 | while True: 247 | # Receive available encrypted data from the peer 248 | encrypted_data = self._sock.recv(self._DEFAULT_BUFFER_SIZE) 249 | 250 | if len(encrypted_data) == 0: 251 | raise IOError("Could not read() - peer closed the connection.") 252 | 253 | # Pass it to the SSL engine 254 | self._network_bio.write(encrypted_data) 255 | 256 | try: 257 | # Try to read the decrypted data 258 | decrypted_data = self._ssl.read(size) 259 | return decrypted_data 260 | 261 | except WantReadError: 262 | # The SSL engine needs more data 263 | # before it can decrypt the whole message 264 | pass 265 | 266 | except OpenSSLError as e: 267 | if "tlsv13 alert certificate required" in str(e): 268 | raise ClientCertificateRequested(self.get_client_CA_list()) 269 | elif "alert bad certificate" in e.args[0]: 270 | # Bad certificate alert (https://github.com/nabla-c0d3/sslyze/issues/532 ) 271 | raise ClientCertificateRequested(self.get_client_CA_list()) 272 | else: 273 | raise 274 | 275 | def write(self, data: bytes) -> int: 276 | """Returns the number of (encrypted) bytes sent.""" 277 | if self._sock is None: 278 | raise IOError("Internal socket set to None; cannot perform handshake.") 279 | if not self._is_handshake_completed: 280 | raise IOError("SSL Handshake was not completed; cannot send data.") 281 | 282 | # Pass the cleartext data to the SSL engine 283 | self._ssl.write(data) 284 | 285 | # Recover the corresponding encrypted data 286 | final_length = self._flush_ssl_engine() 287 | 288 | return final_length 289 | 290 | def _flush_ssl_engine(self) -> int: 291 | if self._sock is None: 292 | raise IOError("Internal socket set to None; cannot perform handshake.") 293 | 294 | length_to_read = self._network_bio.pending() 295 | final_length = length_to_read 296 | while length_to_read: 297 | encrypted_data = self._network_bio.read(length_to_read) 298 | # Send the encrypted data to the peer 299 | self._sock.send(encrypted_data) 300 | length_to_read = self._network_bio.pending() 301 | final_length += length_to_read 302 | 303 | return final_length 304 | 305 | def shutdown(self) -> None: 306 | """Close the TLS connection and the underlying network socket.""" 307 | self._is_handshake_completed = False 308 | try: 309 | self._flush_ssl_engine() 310 | except IOError: 311 | # Ensure shutting down the connection never raises an exception 312 | pass 313 | 314 | try: 315 | self._ssl.shutdown() 316 | except OpenSSLError as e: 317 | # Ignore "uninitialized" exception 318 | if "SSL_shutdown:uninitialized" not in str(e) and "shutdown while in init" not in str(e): 319 | raise 320 | if self._sock: 321 | self._sock.close() 322 | 323 | def set_tlsext_host_name(self, name_indication: str) -> None: 324 | """Set the hostname within the Server Name Indication extension in the client SSL Hello.""" 325 | self._ssl.set_tlsext_host_name(name_indication) 326 | 327 | def set_cipher_list(self, cipher_list: str) -> None: 328 | self._ssl.set_cipher_list(cipher_list) 329 | 330 | def get_cipher_list(self) -> List[str]: 331 | return self._ssl.get_cipher_list() 332 | 333 | def get_current_cipher_name(self) -> str: 334 | return self._ssl.get_cipher_name() 335 | 336 | def get_current_cipher_bits(self) -> int: 337 | return self._ssl.get_cipher_bits() 338 | 339 | def get_ephemeral_key(self) -> Optional[EphemeralKeyInfo]: 340 | try: 341 | dh_info = self._ssl.get_dh_info() 342 | except TypeError: 343 | return None 344 | 345 | if dh_info["type"] == OpenSslEvpPkeyEnum.DH: 346 | return DhEphemeralKeyInfo(**dh_info) 347 | elif dh_info["type"] == OpenSslEvpPkeyEnum.EC: 348 | return NistEcDhKeyExchangeInfo(**dh_info) 349 | elif dh_info["type"] in [OpenSslEvpPkeyEnum.X25519, OpenSslEvpPkeyEnum.X448]: 350 | return EcDhEphemeralKeyInfo(**dh_info) 351 | else: 352 | return None 353 | 354 | def _use_private_key( 355 | self, 356 | client_certificate_chain: Path, 357 | client_key: Path, 358 | client_key_type: OpenSslFileTypeEnum, 359 | client_key_password: str, 360 | ) -> None: 361 | """The certificate chain file must be in PEM format. Private method because it should be set via the 362 | constructor. 363 | """ 364 | # Ensure the files exist 365 | with client_certificate_chain.open(): 366 | pass 367 | with client_key.open(): 368 | pass 369 | 370 | self._ssl_ctx.use_certificate_chain_file(str(client_certificate_chain)) 371 | self._ssl_ctx.set_private_key_password(client_key_password) 372 | try: 373 | self._ssl_ctx.use_PrivateKey_file(str(client_key), client_key_type.value) 374 | except OpenSSLError as e: 375 | if "bad password read" in str(e) or "bad decrypt" in str(e): 376 | raise ValueError("Invalid Private Key") 377 | else: 378 | raise 379 | 380 | self._ssl_ctx.check_private_key() 381 | 382 | _TLSEXT_STATUSTYPE_ocsp = 1 383 | 384 | def set_tlsext_status_ocsp(self) -> None: 385 | """Enable the OCSP Stapling extension.""" 386 | self._ssl.set_tlsext_status_type(self._TLSEXT_STATUSTYPE_ocsp) 387 | 388 | def get_tlsext_status_ocsp_resp(self) -> Optional[_nassl.OCSP_RESPONSE]: 389 | """Retrieve the server's OCSP response. 390 | 391 | Will return None if OCSP Stapling was not enabled before the handshake or if the server did not return 392 | an OCSP response. 393 | 394 | The response can be parsed for example using cryptography: 395 | load_der_ocsp_response(ocsp_resp.as_der_bytes()) 396 | """ 397 | return self._ssl.get_tlsext_status_ocsp_resp() 398 | 399 | def get_client_CA_list(self) -> List[str]: 400 | return self._ssl.get_client_CA_list() 401 | 402 | def get_session(self) -> _nassl.SSL_SESSION: 403 | """Get the SSL connection's Session object.""" 404 | return self._ssl.get_session() 405 | 406 | def set_session(self, ssl_session: _nassl.SSL_SESSION) -> None: 407 | """Set the SSL connection's Session object.""" 408 | self._ssl.set_session(ssl_session) 409 | 410 | _SSL_OP_NO_TICKET = 0x00004000 # No TLS Session tickets 411 | 412 | def disable_stateless_session_resumption(self) -> None: 413 | self._ssl.set_options(self._SSL_OP_NO_TICKET) 414 | 415 | def get_received_chain(self) -> List[str]: 416 | """Returns the PEM-formatted certificate chain as sent by the server. 417 | 418 | The leaf certificate is at index 0. 419 | Each certificate can be parsed using the cryptography module at https://github.com/pyca/cryptography. 420 | """ 421 | return [x509.as_pem() for x509 in self._ssl.get_peer_cert_chain()] 422 | 423 | 424 | class OpenSslEarlyDataStatusEnum(IntEnum): 425 | """Early data status constants.""" 426 | 427 | NOT_SENT = 0 428 | REJECTED = 1 429 | ACCEPTED = 2 430 | 431 | 432 | class ExtendedMasterSecretSupportEnum(IntEnum): 433 | NOT_USED_IN_CURRENT_SESSION = 0 434 | USED_IN_CURRENT_SESSION = 1 435 | UNKNOWN = -1 436 | 437 | 438 | class SslClient(BaseSslClient): 439 | """High level API implementing an SSL client. 440 | 441 | Hostname validation is NOT performed by the SslClient and MUST be implemented at the end of the SSL handshake on the 442 | server's certificate. 443 | """ 444 | 445 | # The default client uses the modern OpenSSL 446 | _NASSL_MODULE = _nassl 447 | 448 | def write_early_data(self, data: bytes) -> int: 449 | """Returns the number of (encrypted) bytes sent.""" 450 | if self._is_handshake_completed: 451 | raise IOError("SSL Handshake was completed; cannot send early data.") 452 | 453 | # Pass the cleartext data to the SSL engine 454 | self._ssl.write_early_data(data) 455 | 456 | # Recover the corresponding encrypted data 457 | final_length = self._flush_ssl_engine() 458 | return final_length 459 | 460 | def get_early_data_status(self) -> OpenSslEarlyDataStatusEnum: 461 | return OpenSslEarlyDataStatusEnum(self._ssl.get_early_data_status()) 462 | 463 | def set_ciphersuites(self, cipher_suites: str) -> None: 464 | """https://github.com/openssl/openssl/pull/5392 465 | .""" 466 | # TODO(AD): Eventually merge this method with get/set_cipher_list() 467 | self._ssl.set_ciphersuites(cipher_suites) 468 | 469 | def set_signature_algorithms(self, algorithms: List[Tuple[OpenSslDigestNidEnum, OpenSslEvpPkeyEnum]]) -> None: 470 | """Set the enabled signature algorithms for the key exchange. 471 | 472 | The algorithms parameter is a list of a public key algorithm and a digest.""" 473 | flattened_sigalgs = [item for sublist in algorithms for item in sublist] 474 | self._ssl.set1_sigalgs(flattened_sigalgs) 475 | 476 | def get_peer_signature_nid(self) -> OpenSslDigestNidEnum: 477 | """Get the digest used for TLS message signing.""" 478 | return OpenSslDigestNidEnum(self._ssl.get_peer_signature_nid()) 479 | 480 | def set_groups(self, supported_groups: List[OpenSslEcNidEnum]) -> None: 481 | """Specify elliptic curves or DH groups that are supported by the client in descending order.""" 482 | self._ssl.set1_groups(supported_groups) 483 | 484 | def get_verified_chain(self) -> List[str]: 485 | """Returns the verified PEM-formatted certificate chain. 486 | 487 | If certificate validation failed, CertificateChainValidationFailed will be raised. 488 | The leaf certificate is at index 0. 489 | Each certificate can be parsed using the cryptography module at https://github.com/pyca/cryptography. 490 | """ 491 | verify_code = self._ssl.get_verify_result() 492 | if verify_code != 0: # X509_V_OK 493 | raise CertificateChainVerificationFailed(verify_code) 494 | 495 | return [x509.as_pem() for x509 in self._ssl.get0_verified_chain()] 496 | 497 | def get_extended_master_secret_support(self) -> ExtendedMasterSecretSupportEnum: 498 | """Indicates whether the current session used extended master secret.""" 499 | support = self._ssl.get_extms_support() 500 | if support == 1: 501 | return ExtendedMasterSecretSupportEnum.USED_IN_CURRENT_SESSION 502 | elif support == 0: 503 | return ExtendedMasterSecretSupportEnum.NOT_USED_IN_CURRENT_SESSION 504 | elif support == -1: 505 | return ExtendedMasterSecretSupportEnum.UNKNOWN 506 | else: 507 | raise ValueError(f"Unexpected return value get_extms_support(): {support}") 508 | -------------------------------------------------------------------------------- /tests/ssl_client_test.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from pathlib import Path 3 | 4 | import pytest 5 | 6 | from nassl import _nassl 7 | from nassl.legacy_ssl_client import LegacySslClient 8 | from nassl.ssl_client import ( 9 | ClientCertificateRequested, 10 | ExtendedMasterSecretSupportEnum, 11 | OpenSslVersionEnum, 12 | OpenSslVerifyEnum, 13 | SslClient, 14 | OpenSSLError, 15 | OpenSslEarlyDataStatusEnum, 16 | OpenSslDigestNidEnum, 17 | ) 18 | from nassl.ephemeral_key_info import ( 19 | OpenSslEvpPkeyEnum, 20 | OpenSslEcNidEnum, 21 | DhEphemeralKeyInfo, 22 | NistEcDhKeyExchangeInfo, 23 | EcDhEphemeralKeyInfo, 24 | ) 25 | from nassl.cert_chain_verifier import CertificateChainVerificationFailed 26 | from tests.openssl_server import ( 27 | ModernOpenSslServer, 28 | ClientAuthConfigEnum, 29 | LegacyOpenSslServer, 30 | ) 31 | 32 | 33 | # TODO(AD): Switch to legacy server and add a TODO; skip tests for TLS 1.3 34 | @pytest.mark.parametrize("ssl_client_cls", [SslClient, LegacySslClient]) 35 | class TestSslClientClientAuthentication: 36 | def test_client_authentication_no_certificate_supplied(self, ssl_client_cls) -> None: 37 | # Given a server that requires client authentication 38 | with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: 39 | # And the client does NOT provide a client certificate 40 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 41 | sock.settimeout(5) 42 | sock.connect((server.hostname, server.port)) 43 | 44 | ssl_client = ssl_client_cls( 45 | ssl_version=OpenSslVersionEnum.TLSV1_2, 46 | underlying_socket=sock, 47 | ssl_verify=OpenSslVerifyEnum.NONE, 48 | ) 49 | # When doing the handshake the right error is returned 50 | with pytest.raises(ClientCertificateRequested): 51 | ssl_client.do_handshake() 52 | 53 | ssl_client.shutdown() 54 | 55 | def test_client_authentication_no_certificate_supplied_but_ignore(self, ssl_client_cls) -> None: 56 | # Given a server that accepts optional client authentication 57 | with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.OPTIONAL) as server: 58 | # And the client does NOT provide a client cert but is configured to ignore the client auth request 59 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 60 | sock.settimeout(5) 61 | sock.connect((server.hostname, server.port)) 62 | 63 | ssl_client = ssl_client_cls( 64 | ssl_version=OpenSslVersionEnum.TLSV1_2, 65 | underlying_socket=sock, 66 | ssl_verify=OpenSslVerifyEnum.NONE, 67 | ignore_client_authentication_requests=True, 68 | ) 69 | # When doing the handshake, it succeeds 70 | try: 71 | ssl_client.do_handshake() 72 | finally: 73 | ssl_client.shutdown() 74 | 75 | def test_client_authentication_succeeds(self, ssl_client_cls) -> None: 76 | # Given a server that requires client authentication 77 | with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: 78 | # And the client provides a client certificate 79 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 80 | sock.settimeout(5) 81 | sock.connect((server.hostname, server.port)) 82 | 83 | ssl_client = ssl_client_cls( 84 | ssl_version=OpenSslVersionEnum.TLSV1_2, 85 | underlying_socket=sock, 86 | ssl_verify=OpenSslVerifyEnum.NONE, 87 | client_certificate_chain=server.get_client_certificate_path(), 88 | client_key=server.get_client_key_path(), 89 | ) 90 | 91 | # When doing the handshake, it succeeds 92 | try: 93 | ssl_client.do_handshake() 94 | finally: 95 | ssl_client.shutdown() 96 | 97 | 98 | @pytest.mark.parametrize("ssl_client_cls", [SslClient, LegacySslClient]) 99 | class TestSslClientOnline: 100 | def test(self, ssl_client_cls) -> None: 101 | # Given an SslClient connecting to Google 102 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 103 | sock.settimeout(5) 104 | sock.connect(("www.google.com", 443)) 105 | 106 | ssl_client = ssl_client_cls( 107 | ssl_version=OpenSslVersionEnum.SSLV23, 108 | underlying_socket=sock, 109 | ssl_verify=OpenSslVerifyEnum.NONE, 110 | ) 111 | 112 | # When doing a TLS handshake, it succeeds 113 | try: 114 | ssl_client.do_handshake() 115 | 116 | # When sending a GET request 117 | ssl_client.write(b"GET / HTTP/1.0\r\n\r\n") 118 | 119 | # It gets a response 120 | assert b"google" in ssl_client.read(1024) 121 | 122 | # And when requesting the server certificate, it returns it 123 | assert ssl_client.get_received_chain() 124 | finally: 125 | ssl_client.shutdown() 126 | 127 | def test_get_dh_info_ecdh(self, ssl_client_cls) -> None: 128 | with LegacyOpenSslServer(cipher="ECDHE-RSA-AES256-SHA") as server: 129 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 130 | sock.settimeout(5) 131 | sock.connect((server.hostname, server.port)) 132 | 133 | ssl_client = ssl_client_cls( 134 | ssl_version=OpenSslVersionEnum.TLSV1_2, 135 | underlying_socket=sock, 136 | ssl_verify=OpenSslVerifyEnum.NONE, 137 | ) 138 | 139 | try: 140 | ssl_client.do_handshake() 141 | finally: 142 | ssl_client.shutdown() 143 | 144 | dh_info = ssl_client.get_ephemeral_key() 145 | 146 | assert isinstance(dh_info, NistEcDhKeyExchangeInfo) 147 | assert dh_info.type == OpenSslEvpPkeyEnum.EC 148 | assert dh_info.size > 0 149 | assert len(dh_info.public_bytes) > 0 150 | assert len(dh_info.x) > 0 151 | assert len(dh_info.y) > 0 152 | 153 | def test_get_dh_info_dh(self, ssl_client_cls) -> None: 154 | with LegacyOpenSslServer(cipher="DHE-RSA-AES256-SHA") as server: 155 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 156 | sock.settimeout(5) 157 | sock.connect((server.hostname, server.port)) 158 | 159 | ssl_client = ssl_client_cls( 160 | ssl_version=OpenSslVersionEnum.TLSV1_2, 161 | underlying_socket=sock, 162 | ssl_verify=OpenSslVerifyEnum.NONE, 163 | ) 164 | 165 | try: 166 | ssl_client.do_handshake() 167 | finally: 168 | ssl_client.shutdown() 169 | 170 | dh_info = ssl_client.get_ephemeral_key() 171 | 172 | assert isinstance(dh_info, DhEphemeralKeyInfo) 173 | assert dh_info.type == OpenSslEvpPkeyEnum.DH 174 | assert dh_info.size > 0 175 | assert len(dh_info.public_bytes) > 0 176 | assert len(dh_info.prime) > 0 177 | assert len(dh_info.generator) > 0 178 | 179 | def test_get_dh_info_no_dh(self, ssl_client_cls) -> None: 180 | with LegacyOpenSslServer(cipher="AES256-SHA") as server: 181 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 182 | sock.settimeout(5) 183 | sock.connect((server.hostname, server.port)) 184 | 185 | ssl_client = ssl_client_cls( 186 | ssl_version=OpenSslVersionEnum.TLSV1_2, 187 | underlying_socket=sock, 188 | ssl_verify=OpenSslVerifyEnum.NONE, 189 | ) 190 | 191 | try: 192 | ssl_client.do_handshake() 193 | finally: 194 | ssl_client.shutdown() 195 | 196 | dh_info = ssl_client.get_ephemeral_key() 197 | 198 | assert dh_info is None 199 | 200 | 201 | class TestModernSslClientOnline: 202 | def test_get_verified_chain(self) -> None: 203 | # Given an SslClient connecting to Google 204 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 205 | sock.settimeout(5) 206 | sock.connect(("www.yahoo.com", 443)) 207 | print(str(Path(__file__).absolute().parent / "google_roots.pem")) 208 | ssl_client = SslClient( 209 | ssl_version=OpenSslVersionEnum.TLSV1_2, 210 | underlying_socket=sock, 211 | # That is configured to properly validate certificates 212 | ssl_verify=OpenSslVerifyEnum.PEER, 213 | ssl_verify_locations=Path(__file__).absolute().parent / "mozilla.pem", 214 | ) 215 | 216 | # When doing a TLS handshake, it succeeds 217 | try: 218 | ssl_client.do_handshake() 219 | 220 | # And when requesting the verified certificate chain, it returns it 221 | assert ssl_client.get_verified_chain() 222 | 223 | finally: 224 | ssl_client.shutdown() 225 | 226 | def test_get_verified_chain_but_validation_failed(self) -> None: 227 | # Given an SslClient connecting to Google 228 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 229 | sock.settimeout(5) 230 | sock.connect(("www.google.com", 443)) 231 | 232 | ssl_client = SslClient( 233 | ssl_version=OpenSslVersionEnum.TLSV1_2, 234 | underlying_socket=sock, 235 | # That is configured to silently fail validation 236 | ssl_verify=OpenSslVerifyEnum.NONE, 237 | ) 238 | 239 | # When doing a TLS handshake, it succeeds 240 | try: 241 | ssl_client.do_handshake() 242 | 243 | # And when requesting the verified certificate chain 244 | with pytest.raises(CertificateChainVerificationFailed): 245 | # It fails because certificate validation failed 246 | ssl_client.get_verified_chain() 247 | finally: 248 | ssl_client.shutdown() 249 | 250 | def test_get_dh_info_ecdh_p256(self) -> None: 251 | with ModernOpenSslServer(cipher="ECDHE-RSA-AES256-SHA", groups="P-256") as server: 252 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 253 | sock.settimeout(5) 254 | sock.connect((server.hostname, server.port)) 255 | 256 | ssl_client = SslClient( 257 | ssl_version=OpenSslVersionEnum.TLSV1_2, 258 | underlying_socket=sock, 259 | ssl_verify=OpenSslVerifyEnum.NONE, 260 | ) 261 | 262 | try: 263 | ssl_client.do_handshake() 264 | finally: 265 | ssl_client.shutdown() 266 | 267 | dh_info = ssl_client.get_ephemeral_key() 268 | 269 | assert isinstance(dh_info, NistEcDhKeyExchangeInfo) 270 | assert dh_info.type == OpenSslEvpPkeyEnum.EC 271 | assert dh_info.size == 256 272 | assert dh_info.curve == OpenSslEcNidEnum.SECP256R1 273 | assert len(dh_info.public_bytes) == 65 274 | assert len(dh_info.x) == 32 275 | assert len(dh_info.y) == 32 276 | 277 | def test_get_dh_info_ecdh_x25519(self) -> None: 278 | with ModernOpenSslServer(cipher="ECDHE-RSA-AES256-SHA", groups="X25519") as server: 279 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 280 | sock.settimeout(5) 281 | sock.connect((server.hostname, server.port)) 282 | 283 | ssl_client = SslClient( 284 | ssl_version=OpenSslVersionEnum.TLSV1_2, 285 | underlying_socket=sock, 286 | ssl_verify=OpenSslVerifyEnum.NONE, 287 | ) 288 | 289 | try: 290 | ssl_client.do_handshake() 291 | finally: 292 | ssl_client.shutdown() 293 | 294 | dh_info = ssl_client.get_ephemeral_key() 295 | 296 | assert isinstance(dh_info, EcDhEphemeralKeyInfo) 297 | assert dh_info.type == OpenSslEvpPkeyEnum.X25519 298 | assert dh_info.size == 253 299 | assert dh_info.curve == OpenSslEcNidEnum.X25519 300 | assert len(dh_info.public_bytes) == 32 301 | 302 | def test_set_groups_curve_secp192k1(self) -> None: 303 | # Given a server that supports a bunch of curves 304 | with ModernOpenSslServer( 305 | cipher="ECDHE-RSA-AES256-SHA", 306 | groups="X25519:prime256v1:secp384r1:secp192k1", 307 | ) as server: 308 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 309 | sock.settimeout(5) 310 | sock.connect((server.hostname, server.port)) 311 | 312 | # And a client that only supports a specific curve: SECP192K1 313 | ssl_client = SslClient( 314 | ssl_version=OpenSslVersionEnum.TLSV1_2, 315 | underlying_socket=sock, 316 | ssl_verify=OpenSslVerifyEnum.NONE, 317 | ) 318 | configured_curve = OpenSslEcNidEnum.SECP192K1 319 | ssl_client.set_groups([configured_curve]) 320 | 321 | # When the client connects to the server 322 | try: 323 | ssl_client.do_handshake() 324 | finally: 325 | ssl_client.shutdown() 326 | 327 | # The curve enabled in the client is the one that was used 328 | dh_info = ssl_client.get_ephemeral_key() 329 | assert isinstance(dh_info, EcDhEphemeralKeyInfo) 330 | assert dh_info.curve == configured_curve 331 | 332 | def test_set_groups_curve_x448(self) -> None: 333 | # Given a server that supports a bunch of curves 334 | with ModernOpenSslServer( 335 | cipher="ECDHE-RSA-AES256-SHA", 336 | groups="X25519:prime256v1:X448:secp384r1:secp192k1", 337 | ) as server: 338 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 339 | sock.settimeout(5) 340 | sock.connect((server.hostname, server.port)) 341 | 342 | # And a client that only supports a specific curve: X448 343 | ssl_client = SslClient( 344 | ssl_version=OpenSslVersionEnum.TLSV1_2, 345 | underlying_socket=sock, 346 | ssl_verify=OpenSslVerifyEnum.NONE, 347 | ) 348 | configured_curve = OpenSslEcNidEnum.X448 349 | ssl_client.set_groups([configured_curve]) 350 | 351 | # When the client connects to the server 352 | try: 353 | ssl_client.do_handshake() 354 | finally: 355 | ssl_client.shutdown() 356 | 357 | # The curve enabled in the client is the one that was used 358 | dh_info = ssl_client.get_ephemeral_key() 359 | assert isinstance(dh_info, EcDhEphemeralKeyInfo) 360 | assert dh_info.curve == configured_curve 361 | assert dh_info.type == OpenSslEvpPkeyEnum.X448 362 | assert dh_info.size == 448 363 | assert len(dh_info.public_bytes) == 56 364 | 365 | def test_get_extended_master_secret_not_used(self) -> None: 366 | # Given a TLS server that does NOT support the Extended Master Secret extension 367 | with LegacyOpenSslServer() as server: 368 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 369 | sock.settimeout(5) 370 | sock.connect((server.hostname, server.port)) 371 | 372 | # When a client connects to it 373 | ssl_client = SslClient( 374 | ssl_version=OpenSslVersionEnum.TLSV1_2, 375 | underlying_socket=sock, 376 | ssl_verify=OpenSslVerifyEnum.NONE, 377 | ) 378 | 379 | # Then, before the handshake, the client cannot tell if Extended Master Secret was used 380 | exms_support_before_handshake = ssl_client.get_extended_master_secret_support() 381 | assert exms_support_before_handshake == ExtendedMasterSecretSupportEnum.UNKNOWN 382 | 383 | try: 384 | ssl_client.do_handshake() 385 | finally: 386 | ssl_client.shutdown() 387 | 388 | # And after the handshake, the client can tell that Extended Master Secret was NOT used 389 | exms_support = ssl_client.get_extended_master_secret_support() 390 | assert exms_support == ExtendedMasterSecretSupportEnum.NOT_USED_IN_CURRENT_SESSION 391 | 392 | def test_get_extended_master_secret_used(self) -> None: 393 | # Given a TLS server that DOES support the Extended Master Secret extension 394 | with ModernOpenSslServer() as server: 395 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 396 | sock.settimeout(5) 397 | sock.connect((server.hostname, server.port)) 398 | 399 | # When a client connects to it 400 | ssl_client = SslClient( 401 | ssl_version=OpenSslVersionEnum.TLSV1_2, 402 | underlying_socket=sock, 403 | ssl_verify=OpenSslVerifyEnum.NONE, 404 | ) 405 | 406 | # Then, before the handshake, the client cannot tell if Extended Master Secret was used 407 | exms_support_before_handshake = ssl_client.get_extended_master_secret_support() 408 | assert exms_support_before_handshake == ExtendedMasterSecretSupportEnum.UNKNOWN 409 | 410 | try: 411 | ssl_client.do_handshake() 412 | finally: 413 | ssl_client.shutdown() 414 | 415 | # And after the handshake, the client can tell that Extended Master Secret was used 416 | exms_support = ssl_client.get_extended_master_secret_support() 417 | assert exms_support == ExtendedMasterSecretSupportEnum.USED_IN_CURRENT_SESSION 418 | 419 | def test_set_signature_algorithms(self) -> None: 420 | # Given a TLS server 421 | with ModernOpenSslServer() as server: 422 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 423 | sock.settimeout(5) 424 | sock.connect((server.hostname, server.port)) 425 | 426 | # And a client 427 | ssl_client = SslClient( 428 | ssl_version=OpenSslVersionEnum.TLSV1_2, 429 | underlying_socket=sock, 430 | ssl_verify=OpenSslVerifyEnum.NONE, 431 | ) 432 | # That's configured to use a specific signature algorithm 433 | ssl_client.set_signature_algorithms([(OpenSslDigestNidEnum.SHA256, OpenSslEvpPkeyEnum.RSA)]) 434 | 435 | # When the client connects to the server, it succeeds 436 | try: 437 | ssl_client.do_handshake() 438 | finally: 439 | ssl_client.shutdown() 440 | 441 | # And the configured signature algorithm was used 442 | assert ssl_client.get_peer_signature_nid() == OpenSslDigestNidEnum.SHA256 443 | 444 | def test_set_signature_algorithms_but_not_supported(self) -> None: 445 | # Given a TLS server 446 | with ModernOpenSslServer() as server: 447 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 448 | sock.settimeout(5) 449 | sock.connect((server.hostname, server.port)) 450 | 451 | # And a client 452 | ssl_client = SslClient( 453 | ssl_version=OpenSslVersionEnum.TLSV1_3, 454 | underlying_socket=sock, 455 | ssl_verify=OpenSslVerifyEnum.NONE, 456 | ) 457 | # That's configured to use signature algorithms that are NOT supported 458 | ssl_client.set_signature_algorithms([(OpenSslDigestNidEnum.SHA512, OpenSslEvpPkeyEnum.EC)]) 459 | 460 | # Then, when the client connects to the server, the handshake fails 461 | with pytest.raises(OpenSSLError, match="handshake failure"): 462 | ssl_client.do_handshake() 463 | ssl_client.shutdown() 464 | 465 | 466 | class TestLegacySslClientOnline: 467 | def test_ssl_2(self) -> None: 468 | # Given a server that supports SSL 2.0 469 | with LegacyOpenSslServer() as server: 470 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 471 | sock.settimeout(5) 472 | sock.connect((server.hostname, server.port)) 473 | 474 | ssl_client = LegacySslClient( 475 | ssl_version=OpenSslVersionEnum.SSLV2, 476 | underlying_socket=sock, 477 | ssl_verify=OpenSslVerifyEnum.NONE, 478 | ignore_client_authentication_requests=True, 479 | ) 480 | # When doing the special SSL 2.0 handshake, it succeeds 481 | try: 482 | ssl_client.do_handshake() 483 | finally: 484 | ssl_client.shutdown() 485 | 486 | 487 | class TestModernSslClientOnlineTls13: 488 | def test(self) -> None: 489 | # Given a server that supports TLS 1.3 490 | with ModernOpenSslServer() as server: 491 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 492 | sock.settimeout(5) 493 | sock.connect((server.hostname, server.port)) 494 | 495 | ssl_client = SslClient( 496 | ssl_version=OpenSslVersionEnum.TLSV1_3, 497 | underlying_socket=sock, 498 | ssl_verify=OpenSslVerifyEnum.NONE, 499 | ) 500 | # When doing the TLS 1.3 handshake, it succeeds 501 | try: 502 | ssl_client.do_handshake() 503 | finally: 504 | ssl_client.shutdown() 505 | 506 | def test_set_ciphersuites(self) -> None: 507 | # Given a server that supports TLS 1.3 508 | with ModernOpenSslServer() as server: 509 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 510 | sock.settimeout(5) 511 | sock.connect((server.hostname, server.port)) 512 | 513 | # And a client that only supports a specific TLS 1.3 cipher suite 514 | ssl_client = SslClient( 515 | ssl_version=OpenSslVersionEnum.TLSV1_3, 516 | underlying_socket=sock, 517 | ssl_verify=OpenSslVerifyEnum.NONE, 518 | ) 519 | ssl_client.set_ciphersuites("TLS_CHACHA20_POLY1305_SHA256") 520 | 521 | # When doing the TLS 1.3 handshake, it succeeds 522 | try: 523 | ssl_client.do_handshake() 524 | finally: 525 | ssl_client.shutdown() 526 | 527 | # And client's cipher suite was used 528 | assert "TLS_CHACHA20_POLY1305_SHA256" == ssl_client.get_current_cipher_name() 529 | 530 | @staticmethod 531 | def _create_tls_1_3_session(server_host: str, server_port: int) -> _nassl.SSL_SESSION: 532 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 533 | sock.settimeout(5) 534 | sock.connect((server_host, server_port)) 535 | 536 | ssl_client = SslClient( 537 | ssl_version=OpenSslVersionEnum.TLSV1_3, 538 | underlying_socket=sock, 539 | ssl_verify=OpenSslVerifyEnum.NONE, 540 | ) 541 | 542 | try: 543 | ssl_client.do_handshake() 544 | ssl_client.write(ModernOpenSslServer.HELLO_MSG) 545 | ssl_client.read(2048) 546 | session = ssl_client.get_session() 547 | 548 | finally: 549 | ssl_client.shutdown() 550 | return session 551 | 552 | def test_write_early_data_does_not_finish_handshake(self) -> None: 553 | # Given a server that supports TLS 1.3 and early data 554 | with ModernOpenSslServer(max_early_data=512) as server: 555 | # That has a previous TLS 1.3 session with the server 556 | session = self._create_tls_1_3_session(server.hostname, server.port) 557 | assert session 558 | 559 | # And the server accepts early data 560 | max_early = session.get_max_early_data() 561 | assert max_early > 0 562 | 563 | # When creating a new connection 564 | sock_early_data = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 565 | sock_early_data.settimeout(5) 566 | sock_early_data.connect((server.hostname, server.port)) 567 | 568 | ssl_client_early_data = SslClient( 569 | ssl_version=OpenSslVersionEnum.TLSV1_3, 570 | underlying_socket=sock_early_data, 571 | ssl_verify=OpenSslVerifyEnum.NONE, 572 | ) 573 | 574 | # That re-uses the previous TLS 1.3 session 575 | ssl_client_early_data.set_session(session) 576 | assert OpenSslEarlyDataStatusEnum.NOT_SENT == ssl_client_early_data.get_early_data_status() 577 | 578 | # When sending early data 579 | ssl_client_early_data.write_early_data(b"EARLY DATA") 580 | 581 | # It succeeds 582 | assert not ssl_client_early_data.is_handshake_completed() 583 | assert OpenSslEarlyDataStatusEnum.REJECTED == ssl_client_early_data.get_early_data_status() 584 | 585 | # And after completing the handshake, the early data was accepted 586 | ssl_client_early_data.do_handshake() 587 | assert OpenSslEarlyDataStatusEnum.ACCEPTED == ssl_client_early_data.get_early_data_status() 588 | 589 | ssl_client_early_data.shutdown() 590 | 591 | def test_write_early_data_fail_when_used_on_non_reused_session(self) -> None: 592 | # Given a server that supports TLS 1.3 and early data 593 | with ModernOpenSslServer(max_early_data=512) as server: 594 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 595 | sock.settimeout(5) 596 | sock.connect((server.hostname, server.port)) 597 | 598 | # That does NOT have a previous session with the server 599 | ssl_client = SslClient( 600 | ssl_version=OpenSslVersionEnum.TLSV1_3, 601 | underlying_socket=sock, 602 | ssl_verify=OpenSslVerifyEnum.NONE, 603 | ) 604 | 605 | # When sending early data 606 | # It fails 607 | with pytest.raises(OpenSSLError, match="you should not call"): 608 | ssl_client.write_early_data(b"EARLY DATA") 609 | 610 | ssl_client.shutdown() 611 | 612 | def test_write_early_data_fail_when_trying_to_send_more_than_max_early_data(self) -> None: 613 | # Given a server that supports TLS 1.3 and early data 614 | with ModernOpenSslServer(max_early_data=1) as server: 615 | # That has a previous TLS 1.3 session with the server 616 | session = self._create_tls_1_3_session(server.hostname, server.port) 617 | assert session 618 | 619 | # And the server only accepts 1 byte of early data 620 | max_early = session.get_max_early_data() 621 | assert 1 == max_early 622 | 623 | # When creating a new connection 624 | sock_early_data = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 625 | sock_early_data.settimeout(5) 626 | sock_early_data.connect((server.hostname, server.port)) 627 | 628 | ssl_client_early_data = SslClient( 629 | ssl_version=OpenSslVersionEnum.TLSV1_3, 630 | underlying_socket=sock_early_data, 631 | ssl_verify=OpenSslVerifyEnum.NONE, 632 | ) 633 | 634 | # That re-uses the previous TLS 1.3 session 635 | ssl_client_early_data.set_session(session) 636 | assert OpenSslEarlyDataStatusEnum.NOT_SENT == ssl_client_early_data.get_early_data_status() 637 | 638 | # When sending too much early data 639 | # It fails 640 | with pytest.raises(OpenSSLError, match="too much early data"): 641 | ssl_client_early_data.write_early_data( 642 | "GET / HTTP/1.1\r\nData: {}\r\n\r\n".format("*" * max_early).encode("ascii") 643 | ) 644 | 645 | ssl_client_early_data.shutdown() 646 | 647 | def test_client_authentication(self) -> None: 648 | # Given a server that requires client authentication 649 | with ModernOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: 650 | # And the client provides an invalid client certificate (actually the server cert) 651 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 652 | sock.settimeout(5) 653 | sock.connect((server.hostname, server.port)) 654 | 655 | ssl_client = SslClient( 656 | ssl_version=OpenSslVersionEnum.TLSV1_3, 657 | underlying_socket=sock, 658 | ssl_verify=OpenSslVerifyEnum.NONE, 659 | ) 660 | 661 | # When doing the handshake the right error is returned 662 | with pytest.raises(ClientCertificateRequested): 663 | ssl_client.do_handshake() 664 | --------------------------------------------------------------------------------