Ответ 1
Фактический SSLContext
ответ ожидается, предположение уже неверно.
См. https://docs.python.org/3/library/ssl.html#ssl.SSLContext.load_verify_locations
Там третий аргумент, cadata
Объект cadata, если он присутствует, является либо строкой ASCII одного или больше PEM-закодированных сертификатов или байтового объекта DER-кодированного сертификаты.
По-видимому, случай с Python 3.4
Получение базового контекста PyObject
Этот простой, ssl.SSLContext
наследует от _ssl._SSLContext
, который в модели данных Python означает, что есть только один объект на одном адресе памяти.
Следовательно, ssl.SSLContext().load_verify_locations(...)
действительно вызовет:
ctx = \
ssl.SSLContext.__new__(<type ssl.SSLContext>, ...) # which calls
self = _ssl._SSLContext.__new__(<type ssl.SSLContext>, ...) # which calls
<type ssl.SSLContext>->tp_alloc() # which understands inheritance
self->ctx = SSL_CTX_new(...) # _ssl fields
self.set_ciphers(...) # ssl fields
return self
_ssl._SSLContext.load_verify_locations(ctx, ...)`.
Реализация C получит объект, казалось бы, неправильный тип, но это ОК, потому что все ожидаемые поля есть, поскольку он был выделен общим type->tp_alloc
, а поля сначала заполнялись _ssl._SSLContext
, а затем ssl.SSLContext
.
Здесь демонстрация (утомительные подробности опущены):
# _parent.c
typedef struct {
PyObject_HEAD
} PyParent;
static PyObject* parent_new(PyTypeObject* type, PyObject* args,
PyObject* kwargs) {
PyParent* self = (PyParent*)type->tp_alloc(type, 0);
printf("Created parent %ld\n", (long)self);
return (PyObject*)self;
}
# child.py
class Child(_parent.Parent):
def foo(self):
print(id(self))
c1 = Child()
print("Created child:", id(c1))
# prints:
Created parent 139990593076080
Created child: 139990593076080
Получение базового контекста OpenSSL
typedef struct {
PyObject_HEAD
SSL_CTX *ctx;
<details skipped>
} PySSLContext;
Таким образом, ctx
находится на известном смещении, которое равно:
PyObject_HEAD
This is a macro which expands to the declarations of the fields of the PyObject type; it is used when declaring new types which represent objects without a varying length. The specific fields it expands to depend on the definition of Py_TRACE_REFS. By default, that macro is not defined, and PyObject_HEAD expands to:
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
When Py_TRACE_REFS is defined, it expands to:
PyObject *_ob_next, *_ob_prev;
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
Таким образом, в производственной (не-отладочной) сборке и принимая во внимание естественное выравнивание, PySSLContext
становится:
struct {
void*;
void*;
SSL_CTX *ctx;
...
}
Таким образом:
_ctx = _ssl._SSLContext(2)
c_ctx = ctypes.cast(id(_ctx), ctypes.POINTER(ctypes.c_void_p))
c_ctx[:3]
[1, 140486908969728, 94916219331584]
# refcnt, type, C ctx
Объединяя все это
import ssl
import socket
import ctypes
import pytest
def contact_github(cafile=""):
ctx = ssl.SSLContext()
ctx.verify_mode = ssl.VerifyMode.CERT_REQUIRED
# ctx.load_verify_locations(cafile, "empty", None) done via ctypes
ssl_ctx = ctypes.cast(id(ctx), ctypes.POINTER(ctypes.c_void_p))[2]
cssl = ctypes.CDLL("/usr/lib/x86_64-linux-gnu/libssl.so.1.1")
cssl.SSL_CTX_load_verify_locations.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
assert cssl.SSL_CTX_load_verify_locations(ssl_ctx, cafile.encode("utf-8"), b"empty")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("github.com", 443))
ss = ctx.wrap_socket(s)
ss.send(b"GET / HTTP/1.0\n\n")
print(ss.recv(1024))
def test_wrong_cert():
with pytest.raises(ssl.SSLError):
contact_github(cafile="bad-cert.pem")
def test_correct_cert():
contact_github(cafile="good-cert.pem")