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

Skip to content

Commit efff706

Browse files
committed
Issue #18138: Implement cadata argument of SSLContext.load_verify_location()
to load CA certificates and CRL from memory. It supports PEM and DER encoded strings.
1 parent e6e2d9b commit efff706

4 files changed

Lines changed: 267 additions & 30 deletions

File tree

Doc/library/ssl.rst

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -821,6 +821,7 @@ to speed up repeated connections from the same clients.
821821

822822
.. versionadded:: 3.4
823823

824+
824825
.. method:: SSLContext.load_cert_chain(certfile, keyfile=None, password=None)
825826

826827
Load a private key and the corresponding certificate. The *certfile*
@@ -851,7 +852,7 @@ to speed up repeated connections from the same clients.
851852
.. versionchanged:: 3.3
852853
New optional argument *password*.
853854

854-
.. method:: SSLContext.load_verify_locations(cafile=None, capath=None)
855+
.. method:: SSLContext.load_verify_locations(cafile=None, capath=None, cadata=None)
855856

856857
Load a set of "certification authority" (CA) certificates used to validate
857858
other peers' certificates when :data:`verify_mode` is other than
@@ -867,6 +868,14 @@ to speed up repeated connections from the same clients.
867868
following an `OpenSSL specific layout
868869
<http://www.openssl.org/docs/ssl/SSL_CTX_load_verify_locations.html>`_.
869870

871+
The *cadata* object, if present, is either an ASCII string of one or more
872+
PEM-encoded certificates or a bytes-like object of DER-encoded
873+
certificates. Like with *capath* extra lines around PEM-encoded
874+
certificates are ignored but at least one certificate must be present.
875+
876+
.. versionchanged:: 3.4
877+
New optional argument *cadata*
878+
870879
.. method:: SSLContext.get_ca_certs(binary_form=False)
871880

872881
Get a list of loaded "certification authority" (CA) certificates. If the

Lib/test/test_ssl.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
PROTOCOLS = sorted(ssl._PROTOCOL_NAMES)
2626
HOST = support.HOST
2727

28-
data_file = lambda name: os.path.join(os.path.dirname(__file__), name)
28+
def data_file(*name):
29+
return os.path.join(os.path.dirname(__file__), *name)
2930

3031
# The custom key and certificate files used in test_ssl are generated
3132
# using Lib/test/make_ssl_certs.py.
@@ -43,6 +44,9 @@
4344
KEY_PASSWORD = "somepass"
4445
CAPATH = data_file("capath")
4546
BYTES_CAPATH = os.fsencode(CAPATH)
47+
CAFILE_NEURONIO = data_file("capath", "4e1295a3.0")
48+
CAFILE_CACERT = data_file("capath", "5ed36f99.0")
49+
4650

4751
# Two keys and certs signed by the same CA (for SNI tests)
4852
SIGNED_CERTFILE = data_file("keycert3.pem")
@@ -726,7 +730,7 @@ def test_load_verify_locations(self):
726730
ctx.load_verify_locations(BYTES_CERTFILE)
727731
ctx.load_verify_locations(cafile=BYTES_CERTFILE, capath=None)
728732
self.assertRaises(TypeError, ctx.load_verify_locations)
729-
self.assertRaises(TypeError, ctx.load_verify_locations, None, None)
733+
self.assertRaises(TypeError, ctx.load_verify_locations, None, None, None)
730734
with self.assertRaises(OSError) as cm:
731735
ctx.load_verify_locations(WRONGCERT)
732736
self.assertEqual(cm.exception.errno, errno.ENOENT)
@@ -738,6 +742,64 @@ def test_load_verify_locations(self):
738742
# Issue #10989: crash if the second argument type is invalid
739743
self.assertRaises(TypeError, ctx.load_verify_locations, None, True)
740744

745+
def test_load_verify_cadata(self):
746+
# test cadata
747+
with open(CAFILE_CACERT) as f:
748+
cacert_pem = f.read()
749+
cacert_der = ssl.PEM_cert_to_DER_cert(cacert_pem)
750+
with open(CAFILE_NEURONIO) as f:
751+
neuronio_pem = f.read()
752+
neuronio_der = ssl.PEM_cert_to_DER_cert(neuronio_pem)
753+
754+
# test PEM
755+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
756+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 0)
757+
ctx.load_verify_locations(cadata=cacert_pem)
758+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 1)
759+
ctx.load_verify_locations(cadata=neuronio_pem)
760+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 2)
761+
# cert already in hash table
762+
ctx.load_verify_locations(cadata=neuronio_pem)
763+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 2)
764+
765+
# combined
766+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
767+
combined = "\n".join((cacert_pem, neuronio_pem))
768+
ctx.load_verify_locations(cadata=combined)
769+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 2)
770+
771+
# with junk around the certs
772+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
773+
combined = ["head", cacert_pem, "other", neuronio_pem, "again",
774+
neuronio_pem, "tail"]
775+
ctx.load_verify_locations(cadata="\n".join(combined))
776+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 2)
777+
778+
# test DER
779+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
780+
ctx.load_verify_locations(cadata=cacert_der)
781+
ctx.load_verify_locations(cadata=neuronio_der)
782+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 2)
783+
# cert already in hash table
784+
ctx.load_verify_locations(cadata=cacert_der)
785+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 2)
786+
787+
# combined
788+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
789+
combined = b"".join((cacert_der, neuronio_der))
790+
ctx.load_verify_locations(cadata=combined)
791+
self.assertEqual(ctx.cert_store_stats()["x509_ca"], 2)
792+
793+
# error cases
794+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
795+
self.assertRaises(TypeError, ctx.load_verify_locations, cadata=object)
796+
797+
with self.assertRaisesRegex(ssl.SSLError, "no start line"):
798+
ctx.load_verify_locations(cadata="broken")
799+
with self.assertRaisesRegex(ssl.SSLError, "not enough data"):
800+
ctx.load_verify_locations(cadata=b"broken")
801+
802+
741803
def test_load_dh_params(self):
742804
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
743805
ctx.load_dh_params(DHFILE)
@@ -1057,6 +1119,28 @@ def test_connect_capath(self):
10571119
finally:
10581120
s.close()
10591121

1122+
def test_connect_cadata(self):
1123+
with open(CAFILE_CACERT) as f:
1124+
pem = f.read()
1125+
der = ssl.PEM_cert_to_DER_cert(pem)
1126+
with support.transient_internet("svn.python.org"):
1127+
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
1128+
ctx.verify_mode = ssl.CERT_REQUIRED
1129+
ctx.load_verify_locations(cadata=pem)
1130+
with ctx.wrap_socket(socket.socket(socket.AF_INET)) as s:
1131+
s.connect(("svn.python.org", 443))
1132+
cert = s.getpeercert()
1133+
self.assertTrue(cert)
1134+
1135+
# same with DER
1136+
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
1137+
ctx.verify_mode = ssl.CERT_REQUIRED
1138+
ctx.load_verify_locations(cadata=der)
1139+
with ctx.wrap_socket(socket.socket(socket.AF_INET)) as s:
1140+
s.connect(("svn.python.org", 443))
1141+
cert = s.getpeercert()
1142+
self.assertTrue(cert)
1143+
10601144
@unittest.skipIf(os.name == "nt", "Can't use a socket as a file under Windows")
10611145
def test_makefile_close(self):
10621146
# Issue #5238: creating a file-like object with makefile() shouldn't

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ Core and Builtins
5959
Library
6060
-------
6161

62+
- Issue #18138: Implement cadata argument of SSLContext.load_verify_location()
63+
to load CA certificates and CRL from memory. It supports PEM and DER
64+
encoded strings.
65+
6266
- Issue #18775: Add name and block_size attribute to HMAC object. They now
6367
provide the same API elements as non-keyed cryptographic hash functions.
6468

Modules/_ssl.c

Lines changed: 167 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2304,60 +2304,200 @@ load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds)
23042304
return NULL;
23052305
}
23062306

2307+
/* internal helper function, returns -1 on error
2308+
*/
2309+
static int
2310+
_add_ca_certs(PySSLContext *self, void *data, Py_ssize_t len,
2311+
int filetype)
2312+
{
2313+
BIO *biobuf = NULL;
2314+
X509_STORE *store;
2315+
int retval = 0, err, loaded = 0;
2316+
2317+
assert(filetype == SSL_FILETYPE_ASN1 || filetype == SSL_FILETYPE_PEM);
2318+
2319+
if (len <= 0) {
2320+
PyErr_SetString(PyExc_ValueError,
2321+
"Empty certificate data");
2322+
return -1;
2323+
} else if (len > INT_MAX) {
2324+
PyErr_SetString(PyExc_OverflowError,
2325+
"Certificate data is too long.");
2326+
return -1;
2327+
}
2328+
2329+
biobuf = BIO_new_mem_buf(data, len);
2330+
if (biobuf == NULL) {
2331+
_setSSLError("Can't allocate buffer", 0, __FILE__, __LINE__);
2332+
return -1;
2333+
}
2334+
2335+
store = SSL_CTX_get_cert_store(self->ctx);
2336+
assert(store != NULL);
2337+
2338+
while (1) {
2339+
X509 *cert = NULL;
2340+
int r;
2341+
2342+
if (filetype == SSL_FILETYPE_ASN1) {
2343+
cert = d2i_X509_bio(biobuf, NULL);
2344+
} else {
2345+
cert = PEM_read_bio_X509(biobuf, NULL,
2346+
self->ctx->default_passwd_callback,
2347+
self->ctx->default_passwd_callback_userdata);
2348+
}
2349+
if (cert == NULL) {
2350+
break;
2351+
}
2352+
r = X509_STORE_add_cert(store, cert);
2353+
X509_free(cert);
2354+
if (!r) {
2355+
err = ERR_peek_last_error();
2356+
if ((ERR_GET_LIB(err) == ERR_LIB_X509) &&
2357+
(ERR_GET_REASON(err) == X509_R_CERT_ALREADY_IN_HASH_TABLE)) {
2358+
/* cert already in hash table, not an error */
2359+
ERR_clear_error();
2360+
} else {
2361+
break;
2362+
}
2363+
}
2364+
loaded++;
2365+
}
2366+
2367+
err = ERR_peek_last_error();
2368+
if ((filetype == SSL_FILETYPE_ASN1) &&
2369+
(loaded > 0) &&
2370+
(ERR_GET_LIB(err) == ERR_LIB_ASN1) &&
2371+
(ERR_GET_REASON(err) == ASN1_R_HEADER_TOO_LONG)) {
2372+
/* EOF ASN1 file, not an error */
2373+
ERR_clear_error();
2374+
retval = 0;
2375+
} else if ((filetype == SSL_FILETYPE_PEM) &&
2376+
(loaded > 0) &&
2377+
(ERR_GET_LIB(err) == ERR_LIB_PEM) &&
2378+
(ERR_GET_REASON(err) == PEM_R_NO_START_LINE)) {
2379+
/* EOF PEM file, not an error */
2380+
ERR_clear_error();
2381+
retval = 0;
2382+
} else {
2383+
_setSSLError(NULL, 0, __FILE__, __LINE__);
2384+
retval = -1;
2385+
}
2386+
2387+
BIO_free(biobuf);
2388+
return retval;
2389+
}
2390+
2391+
23072392
static PyObject *
23082393
load_verify_locations(PySSLContext *self, PyObject *args, PyObject *kwds)
23092394
{
2310-
char *kwlist[] = {"cafile", "capath", NULL};
2311-
PyObject *cafile = NULL, *capath = NULL;
2395+
char *kwlist[] = {"cafile", "capath", "cadata", NULL};
2396+
PyObject *cafile = NULL, *capath = NULL, *cadata = NULL;
23122397
PyObject *cafile_bytes = NULL, *capath_bytes = NULL;
23132398
const char *cafile_buf = NULL, *capath_buf = NULL;
2314-
int r;
2399+
int r = 0, ok = 1;
23152400

23162401
errno = 0;
23172402
if (!PyArg_ParseTupleAndKeywords(args, kwds,
2318-
"|OO:load_verify_locations", kwlist,
2319-
&cafile, &capath))
2403+
"|OOO:load_verify_locations", kwlist,
2404+
&cafile, &capath, &cadata))
23202405
return NULL;
2406+
23212407
if (cafile == Py_None)
23222408
cafile = NULL;
23232409
if (capath == Py_None)
23242410
capath = NULL;
2325-
if (cafile == NULL && capath == NULL) {
2411+
if (cadata == Py_None)
2412+
cadata = NULL;
2413+
2414+
if (cafile == NULL && capath == NULL && cadata == NULL) {
23262415
PyErr_SetString(PyExc_TypeError,
2327-
"cafile and capath cannot be both omitted");
2328-
return NULL;
2416+
"cafile, capath and cadata cannot be all omitted");
2417+
goto error;
23292418
}
23302419
if (cafile && !PyUnicode_FSConverter(cafile, &cafile_bytes)) {
23312420
PyErr_SetString(PyExc_TypeError,
23322421
"cafile should be a valid filesystem path");
2333-
return NULL;
2422+
goto error;
23342423
}
23352424
if (capath && !PyUnicode_FSConverter(capath, &capath_bytes)) {
2336-
Py_XDECREF(cafile_bytes);
23372425
PyErr_SetString(PyExc_TypeError,
23382426
"capath should be a valid filesystem path");
2339-
return NULL;
2427+
goto error;
23402428
}
2341-
if (cafile)
2342-
cafile_buf = PyBytes_AS_STRING(cafile_bytes);
2343-
if (capath)
2344-
capath_buf = PyBytes_AS_STRING(capath_bytes);
2345-
PySSL_BEGIN_ALLOW_THREADS
2346-
r = SSL_CTX_load_verify_locations(self->ctx, cafile_buf, capath_buf);
2347-
PySSL_END_ALLOW_THREADS
2348-
Py_XDECREF(cafile_bytes);
2349-
Py_XDECREF(capath_bytes);
2350-
if (r != 1) {
2351-
if (errno != 0) {
2352-
ERR_clear_error();
2353-
PyErr_SetFromErrno(PyExc_IOError);
2429+
2430+
/* validata cadata type and load cadata */
2431+
if (cadata) {
2432+
Py_buffer buf;
2433+
PyObject *cadata_ascii = NULL;
2434+
2435+
if (PyObject_GetBuffer(cadata, &buf, PyBUF_SIMPLE) == 0) {
2436+
if (!PyBuffer_IsContiguous(&buf, 'C') || buf.ndim > 1) {
2437+
PyBuffer_Release(&buf);
2438+
PyErr_SetString(PyExc_TypeError,
2439+
"cadata should be a contiguous buffer with "
2440+
"a single dimension");
2441+
goto error;
2442+
}
2443+
r = _add_ca_certs(self, buf.buf, buf.len, SSL_FILETYPE_ASN1);
2444+
PyBuffer_Release(&buf);
2445+
if (r == -1) {
2446+
goto error;
2447+
}
2448+
} else {
2449+
PyErr_Clear();
2450+
cadata_ascii = PyUnicode_AsASCIIString(cadata);
2451+
if (cadata_ascii == NULL) {
2452+
PyErr_SetString(PyExc_TypeError,
2453+
"cadata should be a ASCII string or a "
2454+
"bytes-like object");
2455+
goto error;
2456+
}
2457+
r = _add_ca_certs(self,
2458+
PyBytes_AS_STRING(cadata_ascii),
2459+
PyBytes_GET_SIZE(cadata_ascii),
2460+
SSL_FILETYPE_PEM);
2461+
Py_DECREF(cadata_ascii);
2462+
if (r == -1) {
2463+
goto error;
2464+
}
23542465
}
2355-
else {
2356-
_setSSLError(NULL, 0, __FILE__, __LINE__);
2466+
}
2467+
2468+
/* load cafile or capath */
2469+
if (cafile || capath) {
2470+
if (cafile)
2471+
cafile_buf = PyBytes_AS_STRING(cafile_bytes);
2472+
if (capath)
2473+
capath_buf = PyBytes_AS_STRING(capath_bytes);
2474+
PySSL_BEGIN_ALLOW_THREADS
2475+
r = SSL_CTX_load_verify_locations(self->ctx, cafile_buf, capath_buf);
2476+
PySSL_END_ALLOW_THREADS
2477+
if (r != 1) {
2478+
ok = 0;
2479+
if (errno != 0) {
2480+
ERR_clear_error();
2481+
PyErr_SetFromErrno(PyExc_IOError);
2482+
}
2483+
else {
2484+
_setSSLError(NULL, 0, __FILE__, __LINE__);
2485+
}
2486+
goto error;
23572487
}
2488+
}
2489+
goto end;
2490+
2491+
error:
2492+
ok = 0;
2493+
end:
2494+
Py_XDECREF(cafile_bytes);
2495+
Py_XDECREF(capath_bytes);
2496+
if (ok) {
2497+
Py_RETURN_NONE;
2498+
} else {
23582499
return NULL;
23592500
}
2360-
Py_RETURN_NONE;
23612501
}
23622502

23632503
static PyObject *

0 commit comments

Comments
 (0)