I want to be able to pass a certificate to Python's ssl library without requiring a temporary file. It seems that the Python ssl module cannot do that.
To work around this problem I want to retrieve the underlying SSL_CTX
struct stored in the ssl._ssl._SSLContext
class from the native _ssl
module. Using ctypes I could then manually call the respective SSL_CTX_*
functions from libssl with that context. How to do that in C is shown here and I would do the same thing via ctypes.
Unfortunately, I'm stuck at the point where I managed to hook into the load_verify_locations
function from ssl._ssl._SSLContext
but seem to be unable to get the right memory address of the instance of the ssl._ssl._SSLContext
struct. All the load_verify_locations
function is seeing is the parent ssl.SSLContext
object.
My question is, how do I get from an instance of a ssl.SSLContext
object to the memory of the native base class ssl._ssl._SSLContext
? If I would have that, I could easily access its ctx
member.
Here is my code so far. Credits for how to monkeypatch a native Python module go to the forbidden fruit project by Lincoln Clarete
Py_ssize_t = hasattr(ctypes.pythonapi, 'Py_InitModule4_64') and ctypes.c_int64 or ctypes.c_int
class PyObject(ctypes.Structure):
pass
PyObject._fields_ = [
('ob_refcnt', Py_ssize_t),
('ob_type', ctypes.POINTER(PyObject)),
]
class SlotsProxy(PyObject):
_fields_ = [('dict', ctypes.POINTER(PyObject))]
class PySSLContext(ctypes.Structure):
pass
PySSLContext._fields_ = [
('ob_refcnt', Py_ssize_t),
('ob_type', ctypes.POINTER(PySSLContext)),
('ctx', ctypes.c_void_p),
]
name = ssl._ssl._SSLContext.__name__
target = ssl._ssl._SSLContext.__dict__
proxy_dict = SlotsProxy.from_address(id(target))
namespace = {}
ctypes.pythonapi.PyDict_SetItem(
ctypes.py_object(namespace),
ctypes.py_object(name),
proxy_dict.dict,
)
patchable = namespace[name]
old_value = patchable["load_verify_locations"]
libssl = ctypes.cdll.LoadLibrary("libssl.so.1.0.0")
libssl.SSL_CTX_set_verify.argtypes = (ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p)
libssl.SSL_CTX_get_verify_mode.argtypes = (ctypes.c_void_p,)
def load_verify_locations(self, cafile, capath, cadata):
print(self)
print(self.verify_mode)
addr = PySSLContext.from_address(id(self)).ctx
libssl.SSL_CTX_set_verify(addr, 1337, None)
print(libssl.SSL_CTX_get_verify_mode(addr))
print(self.verify_mode)
return old_value(self, cafile, capath, cadata)
patchable["load_verify_locations"] = load_verify_locations
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
The output is:
<ssl.SSLContext object at 0x7f4b81304ba8>
2
1337
2
This suggests, that whatever I'm changing is not the ssl context that Python knows about but some other random memory location.
To try out the code from above, you have to run a https server. Generate a self-signed SSL certificate using:
$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost' -nodes
And start a server using the following code:
import http.server, http.server
import ssl
httpd = http.server.HTTPServer(('localhost', 4443), http.server.SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket (httpd.socket, certfile='cert.pem', keyfile='key.pem', server_side=True)
httpd.serve_forever()
And then add the following line to the end of my example code above:
urllib.request.urlopen("https://localhost:4443", context=context)
Actual
SSLContext
answer forthcoming, the assumption is no longer correct.See https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations
There's a 3rd argument,
cadata
Apparently that's the case since Python 3.4
Getting the underlying PyObject context
This one's easy,
ssl.SSLContext
inherits from_ssl._SSLContext
which in Python data model means that there's just one object at one memory address.Therefore,
ssl.SSLContext().load_verify_locations(...)
will actually call:The C implementation will get an object of seemingly wrong type, but that's OK because all the expected fields are there, as it was allocated by generic
type->tp_alloc
and fields were filled in first by_ssl._SSLContext
and then byssl.SSLContext
.Here's a demonstration (tedious details omitted):
Getting the underlying OpenSSL context
Thus,
ctx
is at a known offset, which is:Thus, in a production (non-debug) build, and taking natural alignment into consideration,
PySSLContext
becomes:Therefore:
Putting it all together