Repository: pyca/cryptography

Issue:
Request for missing binding
In relation to #5449 If possible I would like to request for the following binding to be added: `SSL_CTX_set_client_cert_engine`

It is the only binding missing to be able to use an engine for client certificate requests. Which should apply to engines such a pkcs11 and capi.

In my case using the windows certificate store for mtls

```python
ffi = cffi.FFI()
ffi.cdef("int SSL_CTX_set_client_cert_engine(void *ctx, void *e);")
libssl = ffi.dlopen("libssl-1_1.dll")


def set_client_cert_engine(self:PyOpenSSLContext, engine:VoidType):
    if not libssl.SSL_CTX_set_client_cert_engine(self._ctx._context, engine):
        raise Exception("Was not able to set client cert engine")

PyOpenSSLContext.set_client_cert_engine  = set_client_cert_engine

def load_dynamic_engine(so_path:str):
    if not so_path:
        raise ValueError('SO_PATH for engine not provided')

    if isinstance(so_path, str):
        so_path = so_path.encode('utf-8')

    _lib.ENGINE_load_builtin_engines()
    engine = _lib.ENGINE_by_id(b"dynamic")

    if engine == _ffi.NULL:
        raise ValueError('Could not find dynamic engine')

    if not _lib.ENGINE_ctrl_cmd_string(engine,  b"SO_PATH", so_path, 0):
        raise Exception('Error with engine SO_PATH command: {}'.format(
            so_path.decode('utf-8')))

    if not _lib.ENGINE_ctrl_cmd_string(engine,  b"LOAD", _ffi.NULL, 0):
        raise Exception('Error with engine load commad: {}'.format(
            so_path.decode('utf-8')))

    if not _lib.ENGINE_init(engine):
        _lib.ENGINE_free(engine)
        raise Exception('Cannot initialize engine {}'.format(
            so_path.decode('utf-8')))
    return engine
    
def get_default_chain():
    _ctx = ssl.create_default_context()
    return [ crypto.load_certificate(crypto.FILETYPE_ASN1, cert) for cert in _ctx.get_ca_certs(True)]

ca_certs=get_default_chain()
capipath = os.path.join("C:\\", "Program Files", "OpenSSL-Win64", "bin", "capi.dll")

capi = load_dynamic_engine(capipath)

class WindowsSSLContext(pyopenssl.PyOpenSSLContext):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        _ctx=self._ctx
        _ctx.set_options(ssl.OP_NO_TLSv1_1)
        store=_ctx.get_cert_store()
        for cert in ca_certs:
            store.add_cert(cert)
        self.set_client_cert_engine(capi)
```




Discussion:

Resolving patch:
diff --git a/src/_cffi_src/openssl/ssl.py b/src/_cffi_src/openssl/ssl.py
--- a/src/_cffi_src/openssl/ssl.py
+++ b/src/_cffi_src/openssl/ssl.py
@@ -509,6 +509,12 @@
 int SSL_write_early_data(SSL *, const void *, size_t, size_t *);
 int SSL_read_early_data(SSL *, void *, size_t, size_t *);
 int SSL_CTX_set_max_early_data(SSL_CTX *, uint32_t);
+
+/*
+  Added as an advanced user escape hatch. This symbol is tied to
+  engine support but is declared in ssl.h
+*/
+int SSL_CTX_set_client_cert_engine(SSL_CTX *, ENGINE *);
 """
 
 CUSTOMIZATIONS = """
@@ -517,6 +523,10 @@
 // users have upgraded. PersistentlyDeprecated2020
 static const long Cryptography_HAS_TLSEXT_HOSTNAME = 1;
 
+#ifdef OPENSSL_NO_ENGINE
+int (*SSL_CTX_set_client_cert_engine)(SSL_CTX *, ENGINE *) = NULL;
+#endif
+
 #if CRYPTOGRAPHY_IS_LIBRESSL || CRYPTOGRAPHY_IS_BORINGSSL
 static const long Cryptography_HAS_VERIFIED_CHAIN = 0;
 Cryptography_STACK_OF_X509 *(*SSL_get0_verified_chain)(const SSL *) = NULL;
diff --git a/src/cryptography/hazmat/bindings/openssl/_conditional.py b/src/cryptography/hazmat/bindings/openssl/_conditional.py
--- a/src/cryptography/hazmat/bindings/openssl/_conditional.py
+++ b/src/cryptography/hazmat/bindings/openssl/_conditional.py
@@ -204,6 +204,7 @@ def cryptography_has_engine():
         "ENGINE_load_builtin_engines",
         "ENGINE_load_private_key",
         "ENGINE_load_public_key",
+        "SSL_CTX_set_client_cert_engine",
     ]
 
 


Test patch: