Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a TLS request using a smartcard with python?

I have tried to use python library "requests" to communicate with a website protected by a smartcard. It means a strong authentification in SSL : you must give a client side certificate (cert and private key).

As I am using a smartcard, I cannot read the private key (only the modulus) that is a normal protection. I can read the smartcard with the python library PyKCS11 : all certificate, public key and modulus of private key once given the pin code.

How to mix both requests and PyKCS11 ?
How to make a SSL request with a client side certificate in a smartcard ?

EDIT 2017/08/04

On my Mac :

  • brew install openssl
  • brew install opensc
  • brew install engine_pkcs11
  • openssl
    • engine dynamic -pre SO_PATH:/usr/local/Cellar/engine_pkcs11/0.1.8/lib/engines/engine_pkcs11.so -pre ID:pkcs11 -pre LIST_ADD:1 -pre LOAD -pre MODULE_PATH:/usr/local/lib/(my specific Pkcs11 lib).dylib
      • Loaded: (pkcs11) pkcs11 engine
    • s_client -engine pkcs11 -key '(slot):(id)' -keyform engine -cert 'pem.cer' -connect (host):443 -state -debug
      • SSL handshake ok

My problem now is that pyOpenSSl do not have a function in the API to select an engine (like pkcs11). So I am stopped. I cannot use python.

like image 973
Mlier Avatar asked Jul 29 '17 06:07

Mlier


1 Answers

I had a similar problem except I am on windows and needed to use the "capi" engine for handling smart card client certs. I have got a working code using cffi and requests with pyopenssl, I would hope that changing it to support pkcs11 shouldn't be too difficult.

import os
import ssl
import sys

import cffi
import requests

pyopenssl = requests.packages.urllib3.contrib.pyopenssl
pyopenssl.inject_into_urllib3()

# I use anaconda and these paths are valid for me, change as required
libcryptopath = os.path.join(sys.prefix, "Library", "bin", "libcrypto-1_1-x64.dll")
libsslpath = os.path.join(sys.prefix, "Library", "bin", "libssl-1_1-x64.dll")
capipath = os.path.join(sys.prefix, "Library", "lib", "engines-1_1", "capi.dll")

ffi = cffi.FFI()
ffi.cdef(
    "void *ENGINE_by_id(const char *id);"
    "int ENGINE_ctrl_cmd_string(void *e, const char *cmd_name, const char *arg, int cmd_optional);"
    "int ENGINE_init(void *e);"
    "int SSL_CTX_set_client_cert_engine(void *ctx, void *e);"
)

try:
    libcrypto, libssl, engine
except NameError:
    libcrypto = ffi.dlopen(libcryptopath)
    libssl = ffi.dlopen(libsslpath)
    engine = libcrypto.ENGINE_by_id(b"dynamic")
    libcrypto.ENGINE_ctrl_cmd_string(engine, b"SO_PATH", capipath.encode(), 0)
    libcrypto.ENGINE_ctrl_cmd_string(engine, b"LOAD", ffi.NULL, 0)
    libcrypto.ENGINE_init(engine)


class PyOpenSSLContextCAPI(pyopenssl.PyOpenSSLContext):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        libssl.SSL_CTX_set_client_cert_engine(self._ctx._context, engine)


# https://lukasa.co.uk/2017/02/Configuring_TLS_With_Requests/
class HTTPAdapterCAPI(requests.adapters.HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        context = PyOpenSSLContextCAPI(ssl.PROTOCOL_TLS)
        kwargs['ssl_context'] = context
        return super().init_poolmanager(*args, **kwargs)


class SessionCAPI(requests.Session):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.mount("https://", HTTPAdapterCAPI())


if __name__ == '__main__':
    s = SessionCAPI()
    r = s.get("https://example.com")
    print(r.text)
like image 137
saaketp Avatar answered Nov 17 '22 18:11

saaketp