Skip to content

Commit 666991f

Browse files
authored
bpo-18233: Add internal methods to access peer chain (GH-25467)
The internal `_ssl._SSLSocket` object now provides methods to retrieve the peer cert chain and verified cert chain as a list of Certificate objects. Certificate objects have methods to convert the cert to a dict, PEM, or DER (ASN.1). These are private APIs for now. There is a slim chance to stabilize the approach and provide a public API for 3.10. Otherwise I'll provide a stable API in 3.11. Signed-off-by: Christian Heimes <[email protected]>
1 parent 3c586ca commit 666991f

File tree

9 files changed

+563
-6
lines changed

9 files changed

+563
-6
lines changed

Lib/test/test_ssl.py

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
ctypes = None
3333

3434
ssl = import_helper.import_module("ssl")
35+
import _ssl
3536

3637
from ssl import TLSVersion, _TLSContentType, _TLSMessageType, _TLSAlertType
3738

@@ -297,7 +298,7 @@ def test_wrap_socket(sock, *,
297298
return context.wrap_socket(sock, **kwargs)
298299

299300

300-
def testing_context(server_cert=SIGNED_CERTFILE):
301+
def testing_context(server_cert=SIGNED_CERTFILE, *, server_chain=True):
301302
"""Create context
302303
303304
client_context, server_context, hostname = testing_context()
@@ -316,7 +317,8 @@ def testing_context(server_cert=SIGNED_CERTFILE):
316317

317318
server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
318319
server_context.load_cert_chain(server_cert)
319-
server_context.load_verify_locations(SIGNING_CA)
320+
if server_chain:
321+
server_context.load_verify_locations(SIGNING_CA)
320322

321323
return client_context, server_context, hostname
322324

@@ -2482,6 +2484,12 @@ def run(self):
24822484
elif stripped == b'GETCERT':
24832485
cert = self.sslconn.getpeercert()
24842486
self.write(repr(cert).encode("us-ascii") + b"\n")
2487+
elif stripped == b'VERIFIEDCHAIN':
2488+
certs = self.sslconn._sslobj.get_verified_chain()
2489+
self.write(len(certs).to_bytes(1, "big") + b"\n")
2490+
elif stripped == b'UNVERIFIEDCHAIN':
2491+
certs = self.sslconn._sslobj.get_unverified_chain()
2492+
self.write(len(certs).to_bytes(1, "big") + b"\n")
24852493
else:
24862494
if (support.verbose and
24872495
self.server.connectionchatty):
@@ -4567,6 +4575,63 @@ def test_bpo37428_pha_cert_none(self):
45674575
# server cert has not been validated
45684576
self.assertEqual(s.getpeercert(), {})
45694577

4578+
def test_internal_chain_client(self):
4579+
client_context, server_context, hostname = testing_context(
4580+
server_chain=False
4581+
)
4582+
server = ThreadedEchoServer(context=server_context, chatty=False)
4583+
with server:
4584+
with client_context.wrap_socket(
4585+
socket.socket(),
4586+
server_hostname=hostname
4587+
) as s:
4588+
s.connect((HOST, server.port))
4589+
vc = s._sslobj.get_verified_chain()
4590+
self.assertEqual(len(vc), 2)
4591+
ee, ca = vc
4592+
uvc = s._sslobj.get_unverified_chain()
4593+
self.assertEqual(len(uvc), 1)
4594+
4595+
self.assertEqual(ee, uvc[0])
4596+
self.assertEqual(hash(ee), hash(uvc[0]))
4597+
self.assertEqual(repr(ee), repr(uvc[0]))
4598+
4599+
self.assertNotEqual(ee, ca)
4600+
self.assertNotEqual(hash(ee), hash(ca))
4601+
self.assertNotEqual(repr(ee), repr(ca))
4602+
self.assertNotEqual(ee.get_info(), ca.get_info())
4603+
self.assertIn("CN=localhost", repr(ee))
4604+
self.assertIn("CN=our-ca-server", repr(ca))
4605+
4606+
pem = ee.public_bytes(_ssl.ENCODING_PEM)
4607+
der = ee.public_bytes(_ssl.ENCODING_DER)
4608+
self.assertIsInstance(pem, str)
4609+
self.assertIn("-----BEGIN CERTIFICATE-----", pem)
4610+
self.assertIsInstance(der, bytes)
4611+
self.assertEqual(
4612+
ssl.PEM_cert_to_DER_cert(pem), der
4613+
)
4614+
4615+
def test_internal_chain_server(self):
4616+
client_context, server_context, hostname = testing_context()
4617+
client_context.load_cert_chain(SIGNED_CERTFILE)
4618+
server_context.verify_mode = ssl.CERT_REQUIRED
4619+
server_context.maximum_version = ssl.TLSVersion.TLSv1_2
4620+
4621+
server = ThreadedEchoServer(context=server_context, chatty=False)
4622+
with server:
4623+
with client_context.wrap_socket(
4624+
socket.socket(),
4625+
server_hostname=hostname
4626+
) as s:
4627+
s.connect((HOST, server.port))
4628+
s.write(b'VERIFIEDCHAIN\n')
4629+
res = s.recv(1024)
4630+
self.assertEqual(res, b'\x02\n')
4631+
s.write(b'UNVERIFIEDCHAIN\n')
4632+
res = s.recv(1024)
4633+
self.assertEqual(res, b'\x02\n')
4634+
45704635

45714636
HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename')
45724637
requires_keylog = unittest.skipUnless(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Certificate and PrivateKey classes were added to the ssl module.
2+
Certificates and keys can now be loaded from memory buffer, too.

Modules/_ssl.c

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1706,6 +1706,9 @@ _certificate_to_der(_sslmodulestate *state, X509 *certificate)
17061706
return retval;
17071707
}
17081708

1709+
#include "_ssl/misc.c"
1710+
#include "_ssl/cert.c"
1711+
17091712
/*[clinic input]
17101713
_ssl._test_decode_cert
17111714
path: object(converter="PyUnicode_FSConverter")
@@ -1798,6 +1801,70 @@ _ssl__SSLSocket_getpeercert_impl(PySSLSocket *self, int binary_mode)
17981801
return result;
17991802
}
18001803

1804+
/*[clinic input]
1805+
_ssl._SSLSocket.get_verified_chain
1806+
1807+
[clinic start generated code]*/
1808+
1809+
static PyObject *
1810+
_ssl__SSLSocket_get_verified_chain_impl(PySSLSocket *self)
1811+
/*[clinic end generated code: output=802421163cdc3110 input=5fb0714f77e2bd51]*/
1812+
{
1813+
/* borrowed reference */
1814+
STACK_OF(X509) *chain = SSL_get0_verified_chain(self->ssl);
1815+
if (chain == NULL) {
1816+
Py_RETURN_NONE;
1817+
}
1818+
return _PySSL_CertificateFromX509Stack(self->ctx->state, chain, 1);
1819+
}
1820+
1821+
/*[clinic input]
1822+
_ssl._SSLSocket.get_unverified_chain
1823+
1824+
[clinic start generated code]*/
1825+
1826+
static PyObject *
1827+
_ssl__SSLSocket_get_unverified_chain_impl(PySSLSocket *self)
1828+
/*[clinic end generated code: output=5acdae414e13f913 input=78c33c360c635cb5]*/
1829+
{
1830+
PyObject *retval;
1831+
/* borrowed reference */
1832+
/* TODO: include SSL_get_peer_certificate() for server-side sockets */
1833+
STACK_OF(X509) *chain = SSL_get_peer_cert_chain(self->ssl);
1834+
if (chain == NULL) {
1835+
Py_RETURN_NONE;
1836+
}
1837+
retval = _PySSL_CertificateFromX509Stack(self->ctx->state, chain, 1);
1838+
if (retval == NULL) {
1839+
return NULL;
1840+
}
1841+
/* OpenSSL does not include peer cert for server side connections */
1842+
if (self->socket_type == PY_SSL_SERVER) {
1843+
PyObject *peerobj = NULL;
1844+
X509 *peer = SSL_get_peer_certificate(self->ssl);
1845+
1846+
if (peer == NULL) {
1847+
peerobj = Py_None;
1848+
Py_INCREF(peerobj);
1849+
} else {
1850+
/* consume X509 reference on success */
1851+
peerobj = _PySSL_CertificateFromX509(self->ctx->state, peer, 0);
1852+
if (peerobj == NULL) {
1853+
X509_free(peer);
1854+
Py_DECREF(retval);
1855+
return NULL;
1856+
}
1857+
}
1858+
int res = PyList_Insert(retval, 0, peerobj);
1859+
Py_DECREF(peerobj);
1860+
if (res < 0) {
1861+
Py_DECREF(retval);
1862+
return NULL;
1863+
}
1864+
}
1865+
return retval;
1866+
}
1867+
18011868
static PyObject *
18021869
cipher_to_tuple(const SSL_CIPHER *cipher)
18031870
{
@@ -2809,6 +2876,8 @@ static PyMethodDef PySSLMethods[] = {
28092876
_SSL__SSLSOCKET_COMPRESSION_METHODDEF
28102877
_SSL__SSLSOCKET_SHUTDOWN_METHODDEF
28112878
_SSL__SSLSOCKET_VERIFY_CLIENT_POST_HANDSHAKE_METHODDEF
2879+
_SSL__SSLSOCKET_GET_UNVERIFIED_CHAIN_METHODDEF
2880+
_SSL__SSLSOCKET_GET_VERIFIED_CHAIN_METHODDEF
28122881
{NULL, NULL}
28132882
};
28142883

@@ -5784,6 +5853,10 @@ sslmodule_init_constants(PyObject *m)
57845853
X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS);
57855854
#endif
57865855

5856+
/* file types */
5857+
PyModule_AddIntConstant(m, "ENCODING_PEM", PY_SSL_ENCODING_PEM);
5858+
PyModule_AddIntConstant(m, "ENCODING_DER", PY_SSL_ENCODING_DER);
5859+
57875860
/* protocol versions */
57885861
PyModule_AddIntConstant(m, "PROTO_MINIMUM_SUPPORTED",
57895862
PY_PROTO_MINIMUM_SUPPORTED);
@@ -5986,6 +6059,12 @@ sslmodule_init_types(PyObject *module)
59866059
if (state->PySSLSession_Type == NULL)
59876060
return -1;
59886061

6062+
state->PySSLCertificate_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
6063+
module, &PySSLCertificate_spec, NULL
6064+
);
6065+
if (state->PySSLCertificate_Type == NULL)
6066+
return -1;
6067+
59896068
if (PyModule_AddType(module, state->PySSLContext_Type))
59906069
return -1;
59916070
if (PyModule_AddType(module, state->PySSLSocket_Type))
@@ -5994,7 +6073,8 @@ sslmodule_init_types(PyObject *module)
59946073
return -1;
59956074
if (PyModule_AddType(module, state->PySSLSession_Type))
59966075
return -1;
5997-
6076+
if (PyModule_AddType(module, state->PySSLCertificate_Type))
6077+
return -1;
59986078
return 0;
59996079
}
60006080

@@ -6017,6 +6097,7 @@ sslmodule_traverse(PyObject *m, visitproc visit, void *arg)
60176097
Py_VISIT(state->PySSLSocket_Type);
60186098
Py_VISIT(state->PySSLMemoryBIO_Type);
60196099
Py_VISIT(state->PySSLSession_Type);
6100+
Py_VISIT(state->PySSLCertificate_Type);
60206101
Py_VISIT(state->PySSLErrorObject);
60216102
Py_VISIT(state->PySSLCertVerificationErrorObject);
60226103
Py_VISIT(state->PySSLZeroReturnErrorObject);
@@ -6041,6 +6122,7 @@ sslmodule_clear(PyObject *m)
60416122
Py_CLEAR(state->PySSLSocket_Type);
60426123
Py_CLEAR(state->PySSLMemoryBIO_Type);
60436124
Py_CLEAR(state->PySSLSession_Type);
6125+
Py_CLEAR(state->PySSLCertificate_Type);
60446126
Py_CLEAR(state->PySSLErrorObject);
60456127
Py_CLEAR(state->PySSLCertVerificationErrorObject);
60466128
Py_CLEAR(state->PySSLZeroReturnErrorObject);

Modules/_ssl.h

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
#ifndef Py_SSL_H
22
#define Py_SSL_H
33

4+
/* OpenSSL header files */
5+
#include "openssl/evp.h"
6+
#include "openssl/x509.h"
7+
48
/*
59
* ssl module state
610
*/
@@ -10,6 +14,7 @@ typedef struct {
1014
PyTypeObject *PySSLSocket_Type;
1115
PyTypeObject *PySSLMemoryBIO_Type;
1216
PyTypeObject *PySSLSession_Type;
17+
PyTypeObject *PySSLCertificate_Type;
1318
/* SSL error object */
1419
PyObject *PySSLErrorObject;
1520
PyObject *PySSLCertVerificationErrorObject;
@@ -40,6 +45,30 @@ get_ssl_state(PyObject *module)
4045
(get_ssl_state(_PyType_GetModuleByDef(type, &_sslmodule_def)))
4146
#define get_state_ctx(c) (((PySSLContext *)(c))->state)
4247
#define get_state_sock(s) (((PySSLSocket *)(s))->ctx->state)
43-
#define get_state_mbio(b) ((_sslmodulestate *)PyType_GetModuleState(Py_TYPE(b)))
48+
#define get_state_obj(o) ((_sslmodulestate *)PyType_GetModuleState(Py_TYPE(o)))
49+
#define get_state_mbio(b) get_state_obj(b)
50+
#define get_state_cert(c) get_state_obj(c)
51+
52+
/* ************************************************************************
53+
* certificate
54+
*/
55+
56+
enum py_ssl_encoding {
57+
PY_SSL_ENCODING_PEM=X509_FILETYPE_PEM,
58+
PY_SSL_ENCODING_DER=X509_FILETYPE_ASN1,
59+
PY_SSL_ENCODING_PEM_AUX=X509_FILETYPE_PEM + 0x100,
60+
};
61+
62+
typedef struct {
63+
PyObject_HEAD
64+
X509 *cert;
65+
Py_hash_t hash;
66+
} PySSLCertificate;
67+
68+
/* ************************************************************************
69+
* helpers and utils
70+
*/
71+
static PyObject *_PySSL_BytesFromBIO(_sslmodulestate *state, BIO *bio);
72+
static PyObject *_PySSL_UnicodeFromBIO(_sslmodulestate *state, BIO *bio, const char *error);
4473

4574
#endif /* Py_SSL_H */

0 commit comments

Comments
 (0)