Skip to content

Commit 97a6328

Browse files
committed
bpo-18369: Add certificate and private key types
Signed-off-by: Christian Heimes <christian@python.org>
1 parent 2798f24 commit 97a6328

File tree

14 files changed

+2057
-14
lines changed

14 files changed

+2057
-14
lines changed

Lib/ssl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@
9898
import _ssl # if we can't import it, let the error propagate
9999

100100
from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION
101-
from _ssl import _SSLContext, MemoryBIO, SSLSession
101+
from _ssl import _SSLContext, MemoryBIO, SSLSession, Certificate, PrivateKey
102102
from _ssl import (
103103
SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError,
104104
SSLSyscallError, SSLEOFError, SSLCertVerificationError

Lib/test/test_ssl.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,24 @@ def getpass(self):
14101410
# Make sure the password function isn't called if it isn't needed
14111411
ctx.load_cert_chain(CERTFILE, password=getpass_exception)
14121412

1413+
def test_load_cert_privkey(self):
1414+
chain = ssl.Certificate.chain_from_file(ONLYCERT)
1415+
self.assertEqual(len(chain), 1)
1416+
cas = ssl.Certificate.bundle_from_file(SIGNING_CA)
1417+
self.assertEqual(len(cas), 1)
1418+
pkey = ssl.PrivateKey.from_file(ONLYKEY)
1419+
1420+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1421+
ctx.load_cert_chain(chain, pkey)
1422+
ctx.load_verify_locations(cadata=cas)
1423+
self.assertEqual(len(ctx.get_ca_certs()), 1)
1424+
1425+
pkey = ssl.PrivateKey.from_file(
1426+
ONLYKEY_PROTECTED, password=KEY_PASSWORD
1427+
)
1428+
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
1429+
ctx.load_cert_chain(chain, pkey)
1430+
14131431
def test_load_verify_locations(self):
14141432
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
14151433
ctx.load_verify_locations(CERTFILE)
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: 207 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,25 @@ typedef struct {
321321
PySSLContext *ctx;
322322
} PySSLSession;
323323

324+
<<<<<<< HEAD
325+
||||||| parent of 27501a5ce86 (Work on trust store)
326+
static PyTypeObject *PySSLContext_Type;
327+
static PyTypeObject *PySSLSocket_Type;
328+
static PyTypeObject *PySSLMemoryBIO_Type;
329+
static PyTypeObject *PySSLSession_Type;
330+
static PyTypeObject *PySSLPrivateKey_Type;
331+
static PyTypeObject *PySSLCertificate_Type;
332+
333+
=======
334+
static PyTypeObject *PySSLContext_Type;
335+
static PyTypeObject *PySSLSocket_Type;
336+
static PyTypeObject *PySSLMemoryBIO_Type;
337+
static PyTypeObject *PySSLSession_Type;
338+
static PyTypeObject *PySSLPrivateKey_Type;
339+
static PyTypeObject *PySSLCertificate_Type;
340+
static PyTypeObject *PySSLTrustStore_Type;
341+
342+
>>>>>>> 27501a5ce86 (Work on trust store)
324343
static inline _PySSLError _PySSL_errno(int failed, const SSL *ssl, int retcode)
325344
{
326345
_PySSLError err = { 0 };
@@ -1692,6 +1711,11 @@ _certificate_to_der(_sslmodulestate *state, X509 *certificate)
16921711
return retval;
16931712
}
16941713

1714+
#include "_ssl/misc.c"
1715+
#include "_ssl/cert.c"
1716+
#include "_ssl/pkey.c"
1717+
#include "_ssl/truststore.c"
1718+
16951719
/*[clinic input]
16961720
_ssl._test_decode_cert
16971721
path: object(converter="PyUnicode_FSConverter")
@@ -1784,6 +1808,26 @@ _ssl__SSLSocket_getpeercert_impl(PySSLSocket *self, int binary_mode)
17841808
return result;
17851809
}
17861810

1811+
#if OPENSSL_VERSION_1_1
1812+
/*[clinic input]
1813+
_ssl._SSLSocket.verified_chain
1814+
1815+
[clinic start generated code]*/
1816+
1817+
static PyObject *
1818+
_ssl__SSLSocket_verified_chain_impl(PySSLSocket *self)
1819+
/*[clinic end generated code: output=3068b09c06b0a1eb input=d49eda1fa8963989]*/
1820+
{
1821+
STACK_OF(X509) *chain;
1822+
/* borrowed reference */
1823+
chain = SSL_get0_verified_chain(self->ssl);
1824+
if (chain == NULL) {
1825+
Py_RETURN_NONE;
1826+
}
1827+
return _PySSL_CertificateFromX509Stack(chain, 1);
1828+
}
1829+
#endif
1830+
17871831
static PyObject *
17881832
cipher_to_tuple(const SSL_CIPHER *cipher)
17891833
{
@@ -2799,6 +2843,7 @@ static PyMethodDef PySSLMethods[] = {
27992843
_SSL__SSLSOCKET_COMPRESSION_METHODDEF
28002844
_SSL__SSLSOCKET_SHUTDOWN_METHODDEF
28012845
_SSL__SSLSOCKET_VERIFY_CLIENT_POST_HANDSHAKE_METHODDEF
2846+
_SSL__SSLSOCKET_VERIFIED_CHAIN_METHODDEF
28022847
{NULL, NULL}
28032848
};
28042849

@@ -3521,9 +3566,9 @@ typedef struct {
35213566
int error;
35223567
} _PySSLPasswordInfo;
35233568

3524-
static int
3525-
_pwinfo_set(_PySSLPasswordInfo *pw_info, PyObject* password,
3526-
const char *bad_type_error)
3569+
int
3570+
PySSL_pwinfo_set(PySSLPasswordInfo *pw_info, PyObject* password,
3571+
const char *bad_type_error)
35273572
{
35283573
/* Set the password and size fields of a _PySSLPasswordInfo struct
35293574
from a unicode, bytes, or byte array object.
@@ -3575,13 +3620,14 @@ _pwinfo_set(_PySSLPasswordInfo *pw_info, PyObject* password,
35753620
return 0;
35763621
}
35773622

3578-
static int
3579-
_password_callback(char *buf, int size, int rwflag, void *userdata)
3623+
int
3624+
PySSL_password_cb(char *buf, int size, int rwflag, void *userdata)
35803625
{
3581-
_PySSLPasswordInfo *pw_info = (_PySSLPasswordInfo*) userdata;
3626+
PySSLPasswordInfo *pw_info = (PySSLPasswordInfo*) userdata;
35823627
PyObject *fn_ret = NULL;
35833628

35843629
PySSL_END_ALLOW_THREADS_S(pw_info->thread_state);
3630+
assert(pw_info);
35853631

35863632
if (pw_info->error) {
35873633
/* already failed previously. OpenSSL 3.0.0-alpha14 invokes the
@@ -3598,7 +3644,7 @@ _password_callback(char *buf, int size, int rwflag, void *userdata)
35983644
goto error;
35993645
}
36003646

3601-
if (!_pwinfo_set(pw_info, fn_ret,
3647+
if (!PySSL_pwinfo_set(pw_info, fn_ret,
36023648
"password callback must return a string")) {
36033649
goto error;
36043650
}
@@ -3622,6 +3668,105 @@ _password_callback(char *buf, int size, int rwflag, void *userdata)
36223668
return -1;
36233669
}
36243670

3671+
3672+
static PyObject *
3673+
_load_cert_privkey(PySSLContext *self, PyObject *certs, PyObject *key)
3674+
{
3675+
PyObject *seq = NULL;
3676+
Py_ssize_t i;
3677+
int ret;
3678+
3679+
/* Let's start picky and only accept tuples */
3680+
if (!PyTuple_Check(certs)) {
3681+
PyErr_Format(
3682+
PyExc_TypeError,
3683+
"Expected a tuple of Certificates, got '%.200s'.",
3684+
Py_TYPE(certs)->tp_name
3685+
);
3686+
return NULL;
3687+
}
3688+
if (Py_TYPE(key) != PySSLPrivateKey_Type) {
3689+
PyErr_Format(
3690+
PyExc_TypeError,
3691+
"Expected PrivateKey, got '%.200s'.",
3692+
Py_TYPE(key)->tp_name
3693+
);
3694+
return NULL;
3695+
}
3696+
3697+
seq = PySequence_Fast(certs, "expected a tuple of Certificates");
3698+
if (seq == NULL)
3699+
return NULL;
3700+
if (PySequence_Fast_GET_SIZE(seq) == 0) {
3701+
PyErr_SetString(PyExc_ValueError, "cert list is empty");
3702+
goto error;
3703+
}
3704+
3705+
/* validate certs */
3706+
for (i = 0; i < PySequence_Fast_GET_SIZE(seq); i++) {
3707+
PyObject *ob = PySequence_Fast_GET_ITEM(seq, i);
3708+
if (ob == NULL) {
3709+
goto error;
3710+
}
3711+
if (Py_TYPE(ob) != PySSLCertificate_Type) {
3712+
PyErr_Format(
3713+
PyExc_TypeError,
3714+
"Expected Certificate, got '%.200s'.",
3715+
Py_TYPE(ob)->tp_name
3716+
);
3717+
goto error;
3718+
}
3719+
}
3720+
/* add certs */
3721+
for (i = 0; i < PySequence_Fast_GET_SIZE(seq); i++) {
3722+
/* borrowed reference */
3723+
PySSLCertificate *cert = (PySSLCertificate *)PySequence_Fast_GET_ITEM(seq, i);
3724+
if (cert == NULL) {
3725+
goto error;
3726+
}
3727+
if (i == 0) {
3728+
PySSL_BEGIN_ALLOW_THREADS
3729+
ret = SSL_CTX_use_certificate(self->ctx, cert->cert);
3730+
PySSL_END_ALLOW_THREADS
3731+
if (ERR_peek_error() != 0) {
3732+
_setSSLError(NULL, 0, __FILE__, __LINE__);
3733+
goto error;
3734+
}
3735+
ret = SSL_CTX_clear_chain_certs(self->ctx);
3736+
if (ret == 0) {
3737+
_setSSLError(NULL, 0, __FILE__, __LINE__);
3738+
goto error;
3739+
}
3740+
}
3741+
else {
3742+
/* certs 1..n are chain certs */
3743+
PySSL_BEGIN_ALLOW_THREADS
3744+
ret = SSL_CTX_add1_chain_cert(self->ctx, cert->cert);
3745+
PySSL_END_ALLOW_THREADS
3746+
if (ret == 0) {
3747+
_setSSLError(NULL, 0, __FILE__, __LINE__);
3748+
goto error;
3749+
}
3750+
}
3751+
}
3752+
/* add and verify private key */
3753+
PySSL_BEGIN_ALLOW_THREADS
3754+
ret = SSL_CTX_use_PrivateKey(self->ctx, ((PySSLPrivateKey*)key)->pkey);
3755+
if (ret == 1) {
3756+
/* OK, now match key to cert */
3757+
ret = SSL_CTX_check_private_key(self->ctx);
3758+
}
3759+
PySSL_END_ALLOW_THREADS
3760+
if (ret != 1) {
3761+
_setSSLError(NULL, 0, __FILE__, __LINE__);
3762+
goto error;
3763+
}
3764+
Py_RETURN_NONE;
3765+
error:
3766+
Py_DECREF(seq);
3767+
return NULL;
3768+
}
3769+
36253770
/*[clinic input]
36263771
_ssl._SSLContext.load_cert_chain
36273772
certfile: object
@@ -3638,13 +3783,21 @@ _ssl__SSLContext_load_cert_chain_impl(PySSLContext *self, PyObject *certfile,
36383783
PyObject *certfile_bytes = NULL, *keyfile_bytes = NULL;
36393784
pem_password_cb *orig_passwd_cb = SSL_CTX_get_default_passwd_cb(self->ctx);
36403785
void *orig_passwd_userdata = SSL_CTX_get_default_passwd_cb_userdata(self->ctx);
3641-
_PySSLPasswordInfo pw_info = { NULL, NULL, NULL, 0, 0 };
3786+
PySSLPasswordInfo pw_info = { NULL, NULL, NULL, 0, 0 };
36423787
int r;
36433788

36443789
errno = 0;
36453790
ERR_clear_error();
36463791
if (keyfile == Py_None)
36473792
keyfile = NULL;
3793+
if ((keyfile != NULL) && (Py_TYPE(keyfile) == PySSLPrivateKey_Type)) {
3794+
if (password && password != Py_None) {
3795+
PyErr_SetString(PyExc_ValueError,
3796+
"Cannot use password arg with PrivateKey");
3797+
return NULL;
3798+
}
3799+
return _load_cert_privkey(self, certfile, keyfile);
3800+
}
36483801
if (!PyUnicode_FSConverter(certfile, &certfile_bytes)) {
36493802
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
36503803
PyErr_SetString(PyExc_TypeError,
@@ -3662,11 +3815,11 @@ _ssl__SSLContext_load_cert_chain_impl(PySSLContext *self, PyObject *certfile,
36623815
if (password != Py_None) {
36633816
if (PyCallable_Check(password)) {
36643817
pw_info.callable = password;
3665-
} else if (!_pwinfo_set(&pw_info, password,
3818+
} else if (!PySSL_pwinfo_set(&pw_info, password,
36663819
"password should be a string or callable")) {
36673820
goto error;
36683821
}
3669-
SSL_CTX_set_default_passwd_cb(self->ctx, _password_callback);
3822+
SSL_CTX_set_default_passwd_cb(self->ctx, PySSL_password_cb);
36703823
SSL_CTX_set_default_passwd_cb_userdata(self->ctx, &pw_info);
36713824
}
36723825
PySSL_BEGIN_ALLOW_THREADS_S(pw_info.thread_state);
@@ -3831,6 +3984,7 @@ _ssl__SSLContext_load_verify_locations_impl(PySSLContext *self,
38313984
/*[clinic end generated code: output=454c7e41230ca551 input=42ecfe258233e194]*/
38323985
{
38333986
PyObject *cafile_bytes = NULL, *capath_bytes = NULL;
3987+
PyObject *seq = NULL;
38343988
const char *cafile_buf = NULL, *capath_buf = NULL;
38353989
int r = 0, ok = 1;
38363990

@@ -3899,6 +4053,48 @@ _ssl__SSLContext_load_verify_locations_impl(PySSLContext *self,
38994053
goto error;
39004054
}
39014055
}
4056+
else if (PyList_Check(cadata)) {
4057+
/* List of Certificates instances */
4058+
X509_STORE *store;
4059+
Py_ssize_t i;
4060+
4061+
seq = PySequence_Fast(cadata, "expected a tuple of Certificates");
4062+
if (seq == NULL)
4063+
goto error;
4064+
if (PySequence_Fast_GET_SIZE(seq) == 0) {
4065+
PyErr_SetString(PyExc_ValueError,
4066+
"cadata certificate list is empty");
4067+
goto error;
4068+
}
4069+
store = SSL_CTX_get_cert_store(self->ctx);
4070+
/* validate and add certs */
4071+
for (i = 0; i < PySequence_Fast_GET_SIZE(seq); i++) {
4072+
PySSLCertificate *ob = (PySSLCertificate *)PySequence_Fast_GET_ITEM(seq, i);
4073+
if (ob == NULL) {
4074+
goto error;
4075+
}
4076+
if (Py_TYPE(ob) != PySSLCertificate_Type) {
4077+
PyErr_Format(
4078+
PyExc_TypeError,
4079+
"Expected Certificate in cadata, got '%.200s'.",
4080+
Py_TYPE(ob)->tp_name
4081+
);
4082+
goto error;
4083+
}
4084+
r = X509_STORE_add_cert(store, ob->cert);
4085+
if (!r) {
4086+
int err = ERR_peek_last_error();
4087+
if ((ERR_GET_LIB(err) == ERR_LIB_X509) &&
4088+
(ERR_GET_REASON(err) == X509_R_CERT_ALREADY_IN_HASH_TABLE)) {
4089+
/* cert already in hash table, not an error */
4090+
ERR_clear_error();
4091+
} else {
4092+
_setSSLError(NULL, 0, __FILE__, __LINE__);
4093+
goto error;
4094+
}
4095+
}
4096+
}
4097+
}
39024098
else {
39034099
invalid_cadata:
39044100
PyErr_SetString(PyExc_TypeError,
@@ -3935,6 +4131,7 @@ _ssl__SSLContext_load_verify_locations_impl(PySSLContext *self,
39354131
end:
39364132
Py_XDECREF(cafile_bytes);
39374133
Py_XDECREF(capath_bytes);
4134+
Py_XDECREF(seq);
39384135
if (ok) {
39394136
Py_RETURN_NONE;
39404137
} else {

0 commit comments

Comments
 (0)