diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -751,6 +751,12 @@ :class:`SSLContext` objects have the following methods and attributes: +.. method:: SSLContext.cert_store_stats() + + Get statistics about loaded X509 certs and CRLs. + + .. versionadded:: 3.3 + .. method:: SSLContext.load_cert_chain(certfile, keyfile=None, password=None) Load a private key and the corresponding certificate. The *certfile* @@ -796,6 +802,13 @@ the path to a directory containing several CA certificates in PEM format, following an `OpenSSL specific layout `_. + +.. method:: SSLContext.get_ca_list() + + Get a list of loaded "certification authority" (CA) certificates. Each list + entry is a dict like the output of :meth:`SSLSocket.getpeercert`. The list + does not include certificates from *capath* unless a certificate was + requested and loaded by a SSL connection. .. method:: SSLContext.set_default_verify_paths() diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -630,6 +630,42 @@ gc.collect() self.assertIs(wr(), None) + def test_cert_store_stats(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + self.assertEqual(ctx.cert_store_stats(), {"crl": 0, "x509": 0}) + ctx.load_cert_chain(CERTFILE) + self.assertEqual(ctx.cert_store_stats(), {"crl": 0, "x509": 0}) + ctx.load_cert_chain(CERTFILE, keyfile=CERTFILE) + self.assertEqual(ctx.cert_store_stats(), {"crl": 0, "x509": 0}) + ctx.load_verify_locations(CERTFILE) + self.assertEqual(ctx.cert_store_stats(), {"crl": 0, "x509": 1}) + ctx.load_verify_locations(SVN_PYTHON_ORG_ROOT_CERT) + self.assertEqual(ctx.cert_store_stats(), {"crl": 0, "x509": 2}) + + def test_get_ca_list(self): + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + self.assertEqual(ctx.get_ca_list(), []) + # CERTFILE is not flagged as X509v3 Basic Constraints: CA:TRUE + ctx.load_verify_locations(CERTFILE) + self.assertEqual(ctx.get_ca_list(), []) + self.assertEqual(ctx.cert_store_stats(), {"crl": 0, "x509": 1}) + # but SVN_PYTHON_ORG_ROOT_CERT is a CA cert + ctx.load_verify_locations(SVN_PYTHON_ORG_ROOT_CERT) + self.assertEqual(ctx.cert_store_stats(), {"crl": 0, "x509": 2}) + self.assertEqual(ctx.get_ca_list(), + [{'issuer': ((('organizationName', 'Root CA'),), + (('organizationalUnitName', 'http://www.cacert.org'),), + (('commonName', 'CA Cert Signing Authority'),), + (('emailAddress', 'support@cacert.org'),)), + 'notAfter': 'Mar 29 12:29:49 2033 GMT', + 'notBefore': 'Mar 30 12:29:49 2003 GMT', + 'serialNumber': '00', + 'subject': ((('organizationName', 'Root CA'),), + (('organizationalUnitName', 'http://www.cacert.org'),), + (('commonName', 'CA Cert Signing Authority'),), + (('emailAddress', 'support@cacert.org'),)), + 'version': 3}]) + class SSLErrorTests(unittest.TestCase): diff --git a/Modules/_ssl.c b/Modules/_ssl.c --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -2555,6 +2555,83 @@ #endif } +PyDoc_STRVAR(PySSL_get_stats_doc, +"cert_store_stats() -> {'x509': cnt, 'crl': cnt}\n\ +\n\ +Gets statistic of loaded x509 certs and cert revocation lists inside the\n\ +context's cert store. Certs in a capath directory don't count unless they\n\ +have been used at least once."); + +static PyObject * +cert_store_stats(PySSLContext *self) +{ + X509_STORE *store; + X509_OBJECT *obj; + int x509 = 0, crl = 0, i; + + store = SSL_CTX_get_cert_store(self->ctx); + for (i = 0; i < sk_X509_OBJECT_num(store->objs); i++) { + obj = sk_X509_OBJECT_value(store->objs, i); + switch (obj->type) { + case X509_LU_X509: + x509++; + break; + case X509_LU_CRL: + crl++; + break; + default: + /* FAIL, RETRY, PKEY */ + break; + } + } + return Py_BuildValue("{sisi}", "x509", x509, "crl", crl); +} + +PyDoc_STRVAR(PySSL_get_ca_list_doc, +"get_ca_list -> []\n\ +\n\ +Returns a list of dicts with information of loaded CA certs."); + +static PyObject * +get_ca_list(PySSLContext *self) +{ + X509_STORE *store; + PyObject *ci = NULL, *rlist = NULL; + int i; + + if ((rlist = PyList_New(0)) == NULL) { + return NULL; + } + + store = SSL_CTX_get_cert_store(self->ctx); + for (i = 0; i < sk_X509_OBJECT_num(store->objs); i++) { + X509_OBJECT *obj; + X509 *x509; + + obj = sk_X509_OBJECT_value(store->objs, i); + if (obj->type != X509_LU_X509) { + /* not a x509 cert */ + continue; + } + /* CA for any purpose */ + x509 = obj->data.x509; + if (!X509_check_ca(x509)) { + continue; + } + ci = _decode_certificate(x509); + if (PyList_Append(rlist, ci) == -1) { + goto error; + } + Py_CLEAR(ci); + } + return rlist; + error: + Py_XDECREF(ci); + Py_XDECREF(rlist); + return NULL; +} + + static PyGetSetDef context_getsetlist[] = { {"options", (getter) get_options, (setter) set_options, NULL}, @@ -2586,6 +2663,10 @@ #endif {"set_servername_callback", (PyCFunction) set_servername_callback, METH_VARARGS, PySSL_set_servername_callback_doc}, + {"cert_store_stats", (PyCFunction) cert_store_stats, + METH_NOARGS, PySSL_get_stats_doc}, + {"get_ca_list", (PyCFunction) get_ca_list, + METH_NOARGS, PySSL_get_ca_list_doc}, {NULL, NULL} /* sentinel */ };