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

Skip to content

Commit 4fd1e6a

Browse files
committed
Issue #12803: SSLContext.load_cert_chain() now accepts a password argument
to be used if the private key is encrypted. Patch by Adam Simpkins.
1 parent 2bb371b commit 4fd1e6a

5 files changed

Lines changed: 227 additions & 22 deletions

File tree

Doc/library/ssl.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ to speed up repeated connections from the same clients.
553553

554554
:class:`SSLContext` objects have the following methods and attributes:
555555

556-
.. method:: SSLContext.load_cert_chain(certfile, keyfile=None)
556+
.. method:: SSLContext.load_cert_chain(certfile, keyfile=None, password=None)
557557

558558
Load a private key and the corresponding certificate. The *certfile*
559559
string must be the path to a single file in PEM format containing the
@@ -564,9 +564,25 @@ to speed up repeated connections from the same clients.
564564
:ref:`ssl-certificates` for more information on how the certificate
565565
is stored in the *certfile*.
566566

567+
The *password* argument may be a function to call to get the password for
568+
decrypting the private key. It will only be called if the private key is
569+
encrypted and a password is necessary. It will be called with no arguments,
570+
and it should return a string, bytes, or bytearray. If the return value is
571+
a string it will be encoded as UTF-8 before using it to decrypt the key.
572+
Alternatively a string, bytes, or bytearray value may be supplied directly
573+
as the *password* argument. It will be ignored if the private key is not
574+
encrypted and no password is needed.
575+
576+
If the *password* argument is not specified and a password is required,
577+
OpenSSL's built-in password prompting mechanism will be used to
578+
interactively prompt the user for a password.
579+
567580
An :class:`SSLError` is raised if the private key doesn't
568581
match with the certificate.
569582

583+
.. versionchanged:: 3.3
584+
New optional argument *password*.
585+
570586
.. method:: SSLContext.load_verify_locations(cafile=None, capath=None)
571587

572588
Load a set of "certification authority" (CA) certificates used to validate

Lib/test/test_ssl.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
ONLYKEY = data_file("ssl_key.pem")
4343
BYTES_ONLYCERT = os.fsencode(ONLYCERT)
4444
BYTES_ONLYKEY = os.fsencode(ONLYKEY)
45+
CERTFILE_PROTECTED = data_file("keycert.passwd.pem")
46+
ONLYKEY_PROTECTED = data_file("ssl_key.passwd.pem")
47+
KEY_PASSWORD = "somepass"
4548
CAPATH = data_file("capath")
4649
BYTES_CAPATH = os.fsencode(CAPATH)
4750

@@ -430,6 +433,60 @@ def test_load_cert_chain(self):
430433
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
431434
with self.assertRaisesRegex(ssl.SSLError, "key values mismatch"):
432435
ctx.load_cert_chain(SVN_PYTHON_ORG_ROOT_CERT, ONLYKEY)
436+
# Password protected key and cert
437+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=KEY_PASSWORD)
438+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=KEY_PASSWORD.encode())
439+
ctx.load_cert_chain(CERTFILE_PROTECTED,
440+
password=bytearray(KEY_PASSWORD.encode()))
441+
ctx.load_cert_chain(ONLYCERT, ONLYKEY_PROTECTED, KEY_PASSWORD)
442+
ctx.load_cert_chain(ONLYCERT, ONLYKEY_PROTECTED, KEY_PASSWORD.encode())
443+
ctx.load_cert_chain(ONLYCERT, ONLYKEY_PROTECTED,
444+
bytearray(KEY_PASSWORD.encode()))
445+
with self.assertRaisesRegex(TypeError, "should be a string"):
446+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=True)
447+
with self.assertRaises(ssl.SSLError):
448+
ctx.load_cert_chain(CERTFILE_PROTECTED, password="badpass")
449+
with self.assertRaisesRegex(ValueError, "cannot be longer"):
450+
# openssl has a fixed limit on the password buffer.
451+
# PEM_BUFSIZE is generally set to 1kb.
452+
# Return a string larger than this.
453+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=b'a' * 102400)
454+
# Password callback
455+
def getpass_unicode():
456+
return KEY_PASSWORD
457+
def getpass_bytes():
458+
return KEY_PASSWORD.encode()
459+
def getpass_bytearray():
460+
return bytearray(KEY_PASSWORD.encode())
461+
def getpass_badpass():
462+
return "badpass"
463+
def getpass_huge():
464+
return b'a' * (1024 * 1024)
465+
def getpass_bad_type():
466+
return 9
467+
def getpass_exception():
468+
raise Exception('getpass error')
469+
class GetPassCallable:
470+
def __call__(self):
471+
return KEY_PASSWORD
472+
def getpass(self):
473+
return KEY_PASSWORD
474+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=getpass_unicode)
475+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=getpass_bytes)
476+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=getpass_bytearray)
477+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=GetPassCallable())
478+
ctx.load_cert_chain(CERTFILE_PROTECTED,
479+
password=GetPassCallable().getpass)
480+
with self.assertRaises(ssl.SSLError):
481+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=getpass_badpass)
482+
with self.assertRaisesRegex(ValueError, "cannot be longer"):
483+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=getpass_huge)
484+
with self.assertRaisesRegex(TypeError, "must return a string"):
485+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=getpass_bad_type)
486+
with self.assertRaisesRegex(Exception, "getpass error"):
487+
ctx.load_cert_chain(CERTFILE_PROTECTED, password=getpass_exception)
488+
# Make sure the password function isn't called if it isn't needed
489+
ctx.load_cert_chain(CERTFILE, password=getpass_exception)
433490

434491
def test_load_verify_locations(self):
435492
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,6 +876,7 @@ Eric Siegerman
876876
Paul Sijben
877877
Kirill Simonov
878878
Nathan Paul Simons
879+
Adam Simpkins
879880
Janne Sinkkonen
880881
George Sipe
881882
J. Sipprell

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,9 @@ Core and Builtins
268268
Library
269269
-------
270270

271+
- Issue #12803: SSLContext.load_cert_chain() now accepts a password argument
272+
to be used if the private key is encrypted. Patch by Adam Simpkins.
273+
271274
- Issue #11657: Fix sending file descriptors over 255 over a multiprocessing
272275
Pipe.
273276

Modules/_ssl.c

Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,21 @@
1818

1919
#ifdef WITH_THREAD
2020
#include "pythread.h"
21+
#define PySSL_BEGIN_ALLOW_THREADS_S(save) \
22+
do { if (_ssl_locks_count>0) { (save) = PyEval_SaveThread(); } } while (0)
23+
#define PySSL_END_ALLOW_THREADS_S(save) \
24+
do { if (_ssl_locks_count>0) { PyEval_RestoreThread(save); } } while (0)
2125
#define PySSL_BEGIN_ALLOW_THREADS { \
2226
PyThreadState *_save = NULL; \
23-
if (_ssl_locks_count>0) {_save = PyEval_SaveThread();}
24-
#define PySSL_BLOCK_THREADS if (_ssl_locks_count>0){PyEval_RestoreThread(_save)};
25-
#define PySSL_UNBLOCK_THREADS if (_ssl_locks_count>0){_save = PyEval_SaveThread()};
26-
#define PySSL_END_ALLOW_THREADS if (_ssl_locks_count>0){PyEval_RestoreThread(_save);} \
27-
}
27+
PySSL_BEGIN_ALLOW_THREADS_S(_save);
28+
#define PySSL_BLOCK_THREADS PySSL_END_ALLOW_THREADS_S(_save);
29+
#define PySSL_UNBLOCK_THREADS PySSL_BEGIN_ALLOW_THREADS_S(_save);
30+
#define PySSL_END_ALLOW_THREADS PySSL_END_ALLOW_THREADS_S(_save); }
2831

2932
#else /* no WITH_THREAD */
3033

34+
#define PySSL_BEGIN_ALLOW_THREADS_S(save)
35+
#define PySSL_END_ALLOW_THREADS_S(save)
3136
#define PySSL_BEGIN_ALLOW_THREADS
3237
#define PySSL_BLOCK_THREADS
3338
#define PySSL_UNBLOCK_THREADS
@@ -1635,19 +1640,118 @@ set_options(PySSLContext *self, PyObject *arg, void *c)
16351640
return 0;
16361641
}
16371642

1643+
typedef struct {
1644+
PyThreadState *thread_state;
1645+
PyObject *callable;
1646+
char *password;
1647+
Py_ssize_t size;
1648+
int error;
1649+
} _PySSLPasswordInfo;
1650+
1651+
static int
1652+
_pwinfo_set(_PySSLPasswordInfo *pw_info, PyObject* password,
1653+
const char *bad_type_error)
1654+
{
1655+
/* Set the password and size fields of a _PySSLPasswordInfo struct
1656+
from a unicode, bytes, or byte array object.
1657+
The password field will be dynamically allocated and must be freed
1658+
by the caller */
1659+
PyObject *password_bytes = NULL;
1660+
const char *data = NULL;
1661+
Py_ssize_t size;
1662+
1663+
if (PyUnicode_Check(password)) {
1664+
password_bytes = PyUnicode_AsEncodedString(password, NULL, NULL);
1665+
if (!password_bytes) {
1666+
goto error;
1667+
}
1668+
data = PyBytes_AS_STRING(password_bytes);
1669+
size = PyBytes_GET_SIZE(password_bytes);
1670+
} else if (PyBytes_Check(password)) {
1671+
data = PyBytes_AS_STRING(password);
1672+
size = PyBytes_GET_SIZE(password);
1673+
} else if (PyByteArray_Check(password)) {
1674+
data = PyByteArray_AS_STRING(password);
1675+
size = PyByteArray_GET_SIZE(password);
1676+
} else {
1677+
PyErr_SetString(PyExc_TypeError, bad_type_error);
1678+
goto error;
1679+
}
1680+
1681+
free(pw_info->password);
1682+
pw_info->password = malloc(size);
1683+
if (!pw_info->password) {
1684+
PyErr_SetString(PyExc_MemoryError,
1685+
"unable to allocate password buffer");
1686+
goto error;
1687+
}
1688+
memcpy(pw_info->password, data, size);
1689+
pw_info->size = size;
1690+
1691+
Py_XDECREF(password_bytes);
1692+
return 1;
1693+
1694+
error:
1695+
Py_XDECREF(password_bytes);
1696+
return 0;
1697+
}
1698+
1699+
static int
1700+
_password_callback(char *buf, int size, int rwflag, void *userdata)
1701+
{
1702+
_PySSLPasswordInfo *pw_info = (_PySSLPasswordInfo*) userdata;
1703+
PyObject *fn_ret = NULL;
1704+
1705+
PySSL_END_ALLOW_THREADS_S(pw_info->thread_state);
1706+
1707+
if (pw_info->callable) {
1708+
fn_ret = PyObject_CallFunctionObjArgs(pw_info->callable, NULL);
1709+
if (!fn_ret) {
1710+
/* TODO: It would be nice to move _ctypes_add_traceback() into the
1711+
core python API, so we could use it to add a frame here */
1712+
goto error;
1713+
}
1714+
1715+
if (!_pwinfo_set(pw_info, fn_ret,
1716+
"password callback must return a string")) {
1717+
goto error;
1718+
}
1719+
Py_CLEAR(fn_ret);
1720+
}
1721+
1722+
if (pw_info->size > size) {
1723+
PyErr_Format(PyExc_ValueError,
1724+
"password cannot be longer than %d bytes", size);
1725+
goto error;
1726+
}
1727+
1728+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info->thread_state);
1729+
memcpy(buf, pw_info->password, pw_info->size);
1730+
return pw_info->size;
1731+
1732+
error:
1733+
Py_XDECREF(fn_ret);
1734+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info->thread_state);
1735+
pw_info->error = 1;
1736+
return -1;
1737+
}
1738+
16381739
static PyObject *
16391740
load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds)
16401741
{
1641-
char *kwlist[] = {"certfile", "keyfile", NULL};
1642-
PyObject *certfile, *keyfile = NULL;
1742+
char *kwlist[] = {"certfile", "keyfile", "password", NULL};
1743+
PyObject *certfile, *keyfile = NULL, *password = NULL;
16431744
PyObject *certfile_bytes = NULL, *keyfile_bytes = NULL;
1745+
pem_password_cb *orig_passwd_cb = self->ctx->default_passwd_callback;
1746+
void *orig_passwd_userdata = self->ctx->default_passwd_callback_userdata;
1747+
_PySSLPasswordInfo pw_info = { NULL, NULL, NULL, 0, 0 };
16441748
int r;
16451749

16461750
errno = 0;
16471751
ERR_clear_error();
16481752
if (!PyArg_ParseTupleAndKeywords(args, kwds,
1649-
"O|O:load_cert_chain", kwlist,
1650-
&certfile, &keyfile))
1753+
"O|OO:load_cert_chain", kwlist,
1754+
&certfile, &keyfile, &password))
16511755
return NULL;
16521756
if (keyfile == Py_None)
16531757
keyfile = NULL;
@@ -1661,12 +1765,26 @@ load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds)
16611765
"keyfile should be a valid filesystem path");
16621766
goto error;
16631767
}
1664-
PySSL_BEGIN_ALLOW_THREADS
1768+
if (password && password != Py_None) {
1769+
if (PyCallable_Check(password)) {
1770+
pw_info.callable = password;
1771+
} else if (!_pwinfo_set(&pw_info, password,
1772+
"password should be a string or callable")) {
1773+
goto error;
1774+
}
1775+
SSL_CTX_set_default_passwd_cb(self->ctx, _password_callback);
1776+
SSL_CTX_set_default_passwd_cb_userdata(self->ctx, &pw_info);
1777+
}
1778+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
16651779
r = SSL_CTX_use_certificate_chain_file(self->ctx,
16661780
PyBytes_AS_STRING(certfile_bytes));
1667-
PySSL_END_ALLOW_THREADS
1781+
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state);
16681782
if (r != 1) {
1669-
if (errno != 0) {
1783+
if (pw_info.error) {
1784+
ERR_clear_error();
1785+
/* the password callback has already set the error information */
1786+
}
1787+
else if (errno != 0) {
16701788
ERR_clear_error();
16711789
PyErr_SetFromErrno(PyExc_IOError);
16721790
}
@@ -1675,33 +1793,43 @@ load_cert_chain(PySSLContext *self, PyObject *args, PyObject *kwds)
16751793
}
16761794
goto error;
16771795
}
1678-
PySSL_BEGIN_ALLOW_THREADS
1796+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
16791797
r = SSL_CTX_use_PrivateKey_file(self->ctx,
16801798
PyBytes_AS_STRING(keyfile ? keyfile_bytes : certfile_bytes),
16811799
SSL_FILETYPE_PEM);
1682-
PySSL_END_ALLOW_THREADS
1683-
Py_XDECREF(keyfile_bytes);
1684-
Py_XDECREF(certfile_bytes);
1800+
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state);
1801+
Py_CLEAR(keyfile_bytes);
1802+
Py_CLEAR(certfile_bytes);
16851803
if (r != 1) {
1686-
if (errno != 0) {
1804+
if (pw_info.error) {
1805+
ERR_clear_error();
1806+
/* the password callback has already set the error information */
1807+
}
1808+
else if (errno != 0) {
16871809
ERR_clear_error();
16881810
PyErr_SetFromErrno(PyExc_IOError);
16891811
}
16901812
else {
16911813
_setSSLError(NULL, 0, __FILE__, __LINE__);
16921814
}
1693-
return NULL;
1815+
goto error;
16941816
}
1695-
PySSL_BEGIN_ALLOW_THREADS
1817+
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
16961818
r = SSL_CTX_check_private_key(self->ctx);
1697-
PySSL_END_ALLOW_THREADS
1819+
PySSL_END_ALLOW_THREADS_S(pw_info.thread_state);
16981820
if (r != 1) {
16991821
_setSSLError(NULL, 0, __FILE__, __LINE__);
1700-
return NULL;
1822+
goto error;
17011823
}
1824+
SSL_CTX_set_default_passwd_cb(self->ctx, orig_passwd_cb);
1825+
SSL_CTX_set_default_passwd_cb_userdata(self->ctx, orig_passwd_userdata);
1826+
free(pw_info.password);
17021827
Py_RETURN_NONE;
17031828

17041829
error:
1830+
SSL_CTX_set_default_passwd_cb(self->ctx, orig_passwd_cb);
1831+
SSL_CTX_set_default_passwd_cb_userdata(self->ctx, orig_passwd_userdata);
1832+
free(pw_info.password);
17051833
Py_XDECREF(keyfile_bytes);
17061834
Py_XDECREF(certfile_bytes);
17071835
return NULL;

0 commit comments

Comments
 (0)