Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9a5395a

Browse files
committed
Issue #18147: Add diagnostic functions to ssl.SSLContext().
get_ca_list() lists all loaded CA certificates and cert_store_stats() returns amount of loaded X.509 certs, X.509 CA certs and CRLs.
1 parent 9424bb4 commit 9a5395a

4 files changed

Lines changed: 212 additions & 17 deletions

File tree

Doc/library/ssl.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,19 @@ to speed up repeated connections from the same clients.
791791

792792
:class:`SSLContext` objects have the following methods and attributes:
793793

794+
.. method:: SSLContext.cert_store_stats()
795+
796+
Get statistics about quantities of loaded X.509 certificates, count of
797+
X.509 certificates flagged as CA certificates and certificate revocation
798+
lists as dictionary.
799+
800+
Example for a context with one CA cert and one other cert::
801+
802+
>>> context.cert_store_stats()
803+
{'crl': 0, 'x509_ca': 1, 'x509': 2}
804+
805+
.. versionadded:: 3.4
806+
794807
.. method:: SSLContext.load_cert_chain(certfile, keyfile=None, password=None)
795808

796809
Load a private key and the corresponding certificate. The *certfile*
@@ -837,6 +850,17 @@ to speed up repeated connections from the same clients.
837850
following an `OpenSSL specific layout
838851
<http://www.openssl.org/docs/ssl/SSL_CTX_load_verify_locations.html>`_.
839852

853+
.. method:: SSLContext.get_ca_certs(binary_form=False)
854+
855+
Get a list of loaded "certification authority" (CA) certificates. If the
856+
``binary_form`` parameter is :const:`False` each list
857+
entry is a dict like the output of :meth:`SSLSocket.getpeercert`. Otherwise
858+
the method returns a list of DER-encoded certificates. The returned list
859+
does not contain certificates from *capath* unless a certificate was
860+
requested and loaded by a SSL connection.
861+
862+
..versionadded:: 3.4
863+
840864
.. method:: SSLContext.set_default_verify_paths()
841865

842866
Load a set of default "certification authority" (CA) certificates from

Lib/test/test_ssl.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,47 @@ def dummycallback(sock, servername, ctx, cycle=ctx):
680680
gc.collect()
681681
self.assertIs(wr(), None)
682682

683+
def test_cert_store_stats(self):
684+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
685+
self.assertEqual(ctx.cert_store_stats(),
686+
{'x509_ca': 0, 'crl': 0, 'x509': 0})
687+
ctx.load_cert_chain(CERTFILE)
688+
self.assertEqual(ctx.cert_store_stats(),
689+
{'x509_ca': 0, 'crl': 0, 'x509': 0})
690+
ctx.load_verify_locations(CERTFILE)
691+
self.assertEqual(ctx.cert_store_stats(),
692+
{'x509_ca': 0, 'crl': 0, 'x509': 1})
693+
ctx.load_verify_locations(SVN_PYTHON_ORG_ROOT_CERT)
694+
self.assertEqual(ctx.cert_store_stats(),
695+
{'x509_ca': 1, 'crl': 0, 'x509': 2})
696+
697+
def test_get_ca_certs(self):
698+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
699+
self.assertEqual(ctx.get_ca_certs(), [])
700+
# CERTFILE is not flagged as X509v3 Basic Constraints: CA:TRUE
701+
ctx.load_verify_locations(CERTFILE)
702+
self.assertEqual(ctx.get_ca_certs(), [])
703+
# but SVN_PYTHON_ORG_ROOT_CERT is a CA cert
704+
ctx.load_verify_locations(SVN_PYTHON_ORG_ROOT_CERT)
705+
self.assertEqual(ctx.get_ca_certs(),
706+
[{'issuer': ((('organizationName', 'Root CA'),),
707+
(('organizationalUnitName', 'http://www.cacert.org'),),
708+
(('commonName', 'CA Cert Signing Authority'),),
709+
(('emailAddress', '[email protected]'),)),
710+
'notAfter': asn1time('Mar 29 12:29:49 2033 GMT'),
711+
'notBefore': asn1time('Mar 30 12:29:49 2003 GMT'),
712+
'serialNumber': '00',
713+
'subject': ((('organizationName', 'Root CA'),),
714+
(('organizationalUnitName', 'http://www.cacert.org'),),
715+
(('commonName', 'CA Cert Signing Authority'),),
716+
(('emailAddress', '[email protected]'),)),
717+
'version': 3}])
718+
719+
with open(SVN_PYTHON_ORG_ROOT_CERT) as f:
720+
pem = f.read()
721+
der = ssl.PEM_cert_to_DER_cert(pem)
722+
self.assertEqual(ctx.get_ca_certs(True), [der])
723+
683724

684725
class SSLErrorTests(unittest.TestCase):
685726

@@ -995,6 +1036,22 @@ def test_algorithms(self):
9951036
finally:
9961037
s.close()
9971038

1039+
def test_get_ca_certs_capath(self):
1040+
# capath certs are loaded on request
1041+
with support.transient_internet("svn.python.org"):
1042+
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
1043+
ctx.verify_mode = ssl.CERT_REQUIRED
1044+
ctx.load_verify_locations(capath=CAPATH)
1045+
self.assertEqual(ctx.get_ca_certs(), [])
1046+
s = ctx.wrap_socket(socket.socket(socket.AF_INET))
1047+
s.connect(("svn.python.org", 443))
1048+
try:
1049+
cert = s.getpeercert()
1050+
self.assertTrue(cert)
1051+
finally:
1052+
s.close()
1053+
self.assertEqual(len(ctx.get_ca_certs()), 1)
1054+
9981055

9991056
try:
10001057
import threading

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ Core and Builtins
123123
Library
124124
-------
125125

126+
- Issue #18147: Add diagnostic functions to ssl.SSLContext(). get_ca_list()
127+
lists all loaded CA certificates and cert_store_stats() returns amount of
128+
loaded X.509 certs, X.509 CA certs and CRLs.
129+
126130
- Issue #18076: Introduce importlib.util.decode_source().
127131

128132
- importlib.abc.SourceLoader.get_source() no longer changes SyntaxError or

Modules/_ssl.c

Lines changed: 127 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1023,6 +1023,24 @@ _decode_certificate(X509 *certificate) {
10231023
return NULL;
10241024
}
10251025

1026+
static PyObject *
1027+
_certificate_to_der(X509 *certificate)
1028+
{
1029+
unsigned char *bytes_buf = NULL;
1030+
int len;
1031+
PyObject *retval;
1032+
1033+
bytes_buf = NULL;
1034+
len = i2d_X509(certificate, &bytes_buf);
1035+
if (len < 0) {
1036+
_setSSLError(NULL, 0, __FILE__, __LINE__);
1037+
return NULL;
1038+
}
1039+
/* this is actually an immutable bytes sequence */
1040+
retval = PyBytes_FromStringAndSize((const char *) bytes_buf, len);
1041+
OPENSSL_free(bytes_buf);
1042+
return retval;
1043+
}
10261044

10271045
static PyObject *
10281046
PySSL_test_decode_certificate (PyObject *mod, PyObject *args) {
@@ -1068,8 +1086,6 @@ PySSL_test_decode_certificate (PyObject *mod, PyObject *args) {
10681086
static PyObject *
10691087
PySSL_peercert(PySSLSocket *self, PyObject *args)
10701088
{
1071-
PyObject *retval = NULL;
1072-
int len;
10731089
int verification;
10741090
int binary_mode = 0;
10751091

@@ -1081,21 +1097,7 @@ PySSL_peercert(PySSLSocket *self, PyObject *args)
10811097

10821098
if (binary_mode) {
10831099
/* return cert in DER-encoded format */
1084-
1085-
unsigned char *bytes_buf = NULL;
1086-
1087-
bytes_buf = NULL;
1088-
len = i2d_X509(self->peer_cert, &bytes_buf);
1089-
if (len < 0) {
1090-
PySSL_SetError(self, len, __FILE__, __LINE__);
1091-
return NULL;
1092-
}
1093-
/* this is actually an immutable bytes sequence */
1094-
retval = PyBytes_FromStringAndSize
1095-
((const char *) bytes_buf, len);
1096-
OPENSSL_free(bytes_buf);
1097-
return retval;
1098-
1100+
return _certificate_to_der(self->peer_cert);
10991101
} else {
11001102
verification = SSL_CTX_get_verify_mode(SSL_get_SSL_CTX(self->ssl));
11011103
if ((verification & SSL_VERIFY_PEER) == 0)
@@ -2555,6 +2557,110 @@ set_servername_callback(PySSLContext *self, PyObject *args)
25552557
#endif
25562558
}
25572559

2560+
PyDoc_STRVAR(PySSL_get_stats_doc,
2561+
"cert_store_stats() -> {'crl': int, 'x509_ca': int, 'x509': int}\n\
2562+
\n\
2563+
Returns quantities of loaded X.509 certificates. X.509 certificates with a\n\
2564+
CA extension and certificate revocation lists inside the context's cert\n\
2565+
store.\n\
2566+
NOTE: Certificates in a capath directory aren't loaded unless they have\n\
2567+
been used at least once.");
2568+
2569+
static PyObject *
2570+
cert_store_stats(PySSLContext *self)
2571+
{
2572+
X509_STORE *store;
2573+
X509_OBJECT *obj;
2574+
int x509 = 0, crl = 0, pkey = 0, ca = 0, i;
2575+
2576+
store = SSL_CTX_get_cert_store(self->ctx);
2577+
for (i = 0; i < sk_X509_OBJECT_num(store->objs); i++) {
2578+
obj = sk_X509_OBJECT_value(store->objs, i);
2579+
switch (obj->type) {
2580+
case X509_LU_X509:
2581+
x509++;
2582+
if (X509_check_ca(obj->data.x509)) {
2583+
ca++;
2584+
}
2585+
break;
2586+
case X509_LU_CRL:
2587+
crl++;
2588+
break;
2589+
case X509_LU_PKEY:
2590+
pkey++;
2591+
break;
2592+
default:
2593+
/* Ignore X509_LU_FAIL, X509_LU_RETRY, X509_LU_PKEY.
2594+
* As far as I can tell they are internal states and never
2595+
* stored in a cert store */
2596+
break;
2597+
}
2598+
}
2599+
return Py_BuildValue("{sisisi}", "x509", x509, "crl", crl,
2600+
"x509_ca", ca);
2601+
}
2602+
2603+
PyDoc_STRVAR(PySSL_get_ca_certs_doc,
2604+
"get_ca_certs([der=False]) -> list of loaded certificate\n\
2605+
\n\
2606+
Returns a list of dicts with information of loaded CA certs. If the\n\
2607+
optional argument is True, returns a DER-encoded copy of the CA certificate.\n\
2608+
NOTE: Certificates in a capath directory aren't loaded unless they have\n\
2609+
been used at least once.");
2610+
2611+
static PyObject *
2612+
get_ca_certs(PySSLContext *self, PyObject *args)
2613+
{
2614+
X509_STORE *store;
2615+
PyObject *ci = NULL, *rlist = NULL;
2616+
int i;
2617+
int binary_mode = 0;
2618+
2619+
if (!PyArg_ParseTuple(args, "|p:get_ca_certs", &binary_mode)) {
2620+
return NULL;
2621+
}
2622+
2623+
if ((rlist = PyList_New(0)) == NULL) {
2624+
return NULL;
2625+
}
2626+
2627+
store = SSL_CTX_get_cert_store(self->ctx);
2628+
for (i = 0; i < sk_X509_OBJECT_num(store->objs); i++) {
2629+
X509_OBJECT *obj;
2630+
X509 *cert;
2631+
2632+
obj = sk_X509_OBJECT_value(store->objs, i);
2633+
if (obj->type != X509_LU_X509) {
2634+
/* not a x509 cert */
2635+
continue;
2636+
}
2637+
/* CA for any purpose */
2638+
cert = obj->data.x509;
2639+
if (!X509_check_ca(cert)) {
2640+
continue;
2641+
}
2642+
if (binary_mode) {
2643+
ci = _certificate_to_der(cert);
2644+
} else {
2645+
ci = _decode_certificate(cert);
2646+
}
2647+
if (ci == NULL) {
2648+
goto error;
2649+
}
2650+
if (PyList_Append(rlist, ci) == -1) {
2651+
goto error;
2652+
}
2653+
Py_CLEAR(ci);
2654+
}
2655+
return rlist;
2656+
2657+
error:
2658+
Py_XDECREF(ci);
2659+
Py_XDECREF(rlist);
2660+
return NULL;
2661+
}
2662+
2663+
25582664
static PyGetSetDef context_getsetlist[] = {
25592665
{"options", (getter) get_options,
25602666
(setter) set_options, NULL},
@@ -2586,6 +2692,10 @@ static struct PyMethodDef context_methods[] = {
25862692
#endif
25872693
{"set_servername_callback", (PyCFunction) set_servername_callback,
25882694
METH_VARARGS, PySSL_set_servername_callback_doc},
2695+
{"cert_store_stats", (PyCFunction) cert_store_stats,
2696+
METH_NOARGS, PySSL_get_stats_doc},
2697+
{"get_ca_certs", (PyCFunction) get_ca_certs,
2698+
METH_VARARGS, PySSL_get_ca_certs_doc},
25892699
{NULL, NULL} /* sentinel */
25902700
};
25912701

0 commit comments

Comments
 (0)