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

Skip to content

Commit 8586656

Browse files
committed
bpo-18233: Add SSLSocket.getpeercertchain()
Based on the patch provided by Christian Heimes (christian.heimes) and updated by Mariusz Masztalerczuk (mmasztalerczuk).
1 parent 09c580c commit 8586656

File tree

5 files changed

+237
-1
lines changed

5 files changed

+237
-1
lines changed

Doc/library/ssl.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,21 @@ SSL sockets also have the following additional methods and attributes:
12591259
.. versionchanged:: 3.9
12601260
IPv6 address strings no longer have a trailing new line.
12611261

1262+
.. method:: SSLSocket.getpeercertchain(binary_form=False, validate=True)
1263+
1264+
Returns certificate chain for the peer. If no chain is provided, returns
1265+
None. Otherwise returns a tuple of dicts containing information about the
1266+
certificates. The chain starts with the leaf certificate and ends with the
1267+
root certificate. If called on the client side, the leaf certificate is the
1268+
peer's certificate.
1269+
1270+
If the optional argument *binary_form* is True, return a list of *binary_form*-encoded copies
1271+
of the certificates.
1272+
If the optional argument *validate* is False, return the peer's cert chain
1273+
without any validation and without the root CA cert.");
1274+
1275+
.. versionadded:: 3.9
1276+
12621277
.. method:: SSLSocket.cipher()
12631278

12641279
Returns a three-value tuple containing the name of the cipher being used, the

Lib/ssl.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,13 @@ def getpeercert(self, binary_form=False):
905905
"""
906906
return self._sslobj.getpeercert(binary_form)
907907

908+
def getpeercertchain(self, binary_form=False, validate=True):
909+
""""Returns the certificate chain of the SSL connection
910+
as tuple of dicts.
911+
912+
Return None if no chain is provieded."""
913+
return self._sslobj.getpeercertchain(binary_form, validate)
914+
908915
def selected_npn_protocol(self):
909916
"""Return the currently selected NPN protocol as a string, or ``None``
910917
if a next protocol was not negotiated or if NPN is not supported by one
@@ -1123,6 +1130,12 @@ def getpeercert(self, binary_form=False):
11231130
self._check_connected()
11241131
return self._sslobj.getpeercert(binary_form)
11251132

1133+
@_sslcopydoc
1134+
def getpeercertchain(self, binary_form=False, validate=True):
1135+
self._checkClosed()
1136+
self._check_connected()
1137+
return self._sslobj.getpeercertchain(binary_form, validate)
1138+
11261139
@_sslcopydoc
11271140
def selected_npn_protocol(self):
11281141
self._checkClosed()

Lib/test/test_ssl.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,36 @@ def test_get_ca_certs_capath(self):
21372137
self.assertTrue(cert)
21382138
self.assertEqual(len(ctx.get_ca_certs()), 1)
21392139

2140+
def test_getpeercertchain(self):
2141+
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
2142+
ctx.verify_mode = ssl.CERT_REQUIRED
2143+
ctx.load_verify_locations(capath=CAPATH)
2144+
with ctx.wrap_socket(socket.socket(socket.AF_INET)) as s:
2145+
s.connect(self.server_addr)
2146+
try:
2147+
peer_cert = s.getpeercert()
2148+
peer_cert_bin = s.getpeercert(True)
2149+
chain = s.getpeercertchain()
2150+
chain_bin = s.getpeercertchain(True)
2151+
chain_no_validate = s.getpeercertchain(validate=False)
2152+
chain_bin_no_validate = s.getpeercertchain(True, False)
2153+
finally:
2154+
self.assertTrue(peer_cert)
2155+
self.assertEqual(len(chain), 2)
2156+
self.assertTrue(peer_cert_bin)
2157+
self.assertEqual(len(chain_bin), 2)
2158+
2159+
# ca cert
2160+
ca_certs = ctx.get_ca_certs()
2161+
self.assertEqual(len(ca_certs), 1)
2162+
test_get_ca_certsert = ca_certs[0]
2163+
ca_cert_bin = ctx.get_ca_certs(True)[0]
2164+
2165+
self.assertEqual(chain, (peer_cert, test_get_ca_certsert))
2166+
self.assertEqual(chain_bin, (peer_cert_bin, ca_cert_bin))
2167+
self.assertEqual(chain_no_validate, (peer_cert,))
2168+
self.assertEqual(chain_bin_no_validate, (peer_cert_bin,))
2169+
21402170
@needs_sni
21412171
def test_context_setget(self):
21422172
# Check that the context of a connected socket can be replaced.

Modules/_ssl.c

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,6 +2082,133 @@ _ssl__SSLSocket_cipher_impl(PySSLSocket *self)
20822082
return cipher_to_tuple(current);
20832083
}
20842084

2085+
/*[clinic input]
2086+
_ssl._SSLSocket.getpeercertchain
2087+
der as binary_mode: bool = False
2088+
validate: bool = True
2089+
[clinic start generated code]*/
2090+
2091+
static PyObject *
2092+
_ssl__SSLSocket_getpeercertchain_impl(PySSLSocket *self, int binary_mode,
2093+
int validate)
2094+
/*[clinic end generated code: output=8094e6d78d27eb9a input=f4dcd181d0d163eb]*/
2095+
{
2096+
int len, i;
2097+
PyObject *retval = NULL, *ci=NULL;
2098+
STACK_OF(X509) *peer_chain; /* reference */
2099+
2100+
assert((self->ctx != NULL) && (self->ctx->ctx != NULL));
2101+
if (self->ssl == NULL) {
2102+
Py_RETURN_NONE;
2103+
}
2104+
2105+
/* The peer just transmits the intermediate cert chain EXCLUDING the root
2106+
* CA certificate as this side is suppose to have a copy of the root
2107+
* certificate for verification. */
2108+
if (validate) {
2109+
#ifdef OPENSSL_VERSION_1_1
2110+
peer_chain = SSL_get0_verified_chain(self->ssl);
2111+
long ret = SSL_get_verify_result(self->ssl);
2112+
if (ret != X509_V_OK) {
2113+
#ifdef SSL_R_CERTIFICATE_VERIFY_FAILED
2114+
long e = ERR_PACK(ERR_LIB_SSL, 0, SSL_R_CERTIFICATE_VERIFY_FAILED);
2115+
#else
2116+
long e = ERR_PACK(ERR_LIB_SSL, 0, 134);
2117+
#endif
2118+
fill_and_set_sslerror(self, PySSLCertVerificationErrorObject, PY_SSL_ERROR_SSL, NULL, __LINE__, e);
2119+
return NULL;
2120+
}
2121+
#else
2122+
X509 *peer_cert = SSL_get_peer_certificate(self->ssl);
2123+
if (peer_cert == NULL)
2124+
Py_RETURN_NONE;
2125+
2126+
STACK_OF(X509) *chain = SSL_get_peer_cert_chain(self->ssl);
2127+
if (chain == NULL) {
2128+
X509_free(peer_cert);
2129+
Py_RETURN_NONE;
2130+
}
2131+
X509_STORE_CTX *store_ctx;
2132+
2133+
/* Initialize a store context with store (for root CA certs), the
2134+
* peer's cert and the peer's chain with intermediate CA certs. */
2135+
if ((store_ctx = X509_STORE_CTX_new()) == NULL) {
2136+
X509_free(peer_cert);
2137+
_setSSLError(NULL, 0, __FILE__, __LINE__);
2138+
return NULL;
2139+
}
2140+
2141+
if (!X509_STORE_CTX_init(store_ctx,
2142+
SSL_CTX_get_cert_store(self->ctx->ctx),
2143+
peer_cert, chain)) {
2144+
#ifdef SSL_R_CERTIFICATE_VERIFY_FAILED
2145+
long e = ERR_PACK(ERR_LIB_SSL, 0, SSL_R_CERTIFICATE_VERIFY_FAILED);
2146+
#else
2147+
long e = ERR_PACK(ERR_LIB_SSL, 0, 134);
2148+
#endif
2149+
fill_and_set_sslerror(self, PySSLCertVerificationErrorObject, PY_SSL_ERROR_SSL, NULL, __LINE__, e);
2150+
X509_free(peer_cert);
2151+
X509_STORE_CTX_free(store_ctx);
2152+
goto end;
2153+
}
2154+
X509_free(peer_cert);
2155+
2156+
/* Validate peer cert using its intermediate CA certs and the
2157+
* context's root CA certs. */
2158+
if (X509_verify_cert(store_ctx) <= 0) {
2159+
// _setX509StoreContextError(self, store_ctx, __FILE__, __LINE__);
2160+
#ifdef SSL_R_CERTIFICATE_VERIFY_FAILED
2161+
long e = ERR_PACK(ERR_LIB_SSL, 0, SSL_R_CERTIFICATE_VERIFY_FAILED);
2162+
#else
2163+
long e = ERR_PACK(ERR_LIB_SSL, 0, 134);
2164+
#endif
2165+
fill_and_set_sslerror(self, PySSLCertVerificationErrorObject, PY_SSL_ERROR_SSL, NULL, __LINE__, e);
2166+
X509_STORE_CTX_free(store_ctx);
2167+
goto end;
2168+
}
2169+
2170+
/* Get chain from store context */
2171+
peer_chain = X509_STORE_CTX_get1_chain(store_ctx);
2172+
X509_STORE_CTX_free(store_ctx);
2173+
#endif
2174+
} else {
2175+
peer_chain = SSL_get_peer_cert_chain(self->ssl);
2176+
}
2177+
2178+
if (peer_chain == NULL) {
2179+
Py_RETURN_NONE;
2180+
}
2181+
2182+
len = sk_X509_num(peer_chain);
2183+
2184+
if ((retval = PyTuple_New(len)) == NULL) {
2185+
return NULL;
2186+
}
2187+
2188+
for (i = 0; i < len; i++){
2189+
X509 *cert = sk_X509_value(peer_chain, i);
2190+
if (binary_mode) {
2191+
ci = _certificate_to_der(cert);
2192+
} else {
2193+
ci = _decode_certificate(cert);
2194+
}
2195+
2196+
if (ci == NULL) {
2197+
Py_CLEAR(retval);
2198+
goto end;
2199+
}
2200+
PyTuple_SET_ITEM(retval, i, ci);
2201+
}
2202+
2203+
end:
2204+
#ifndef OPENSSL_VERSION_1_1
2205+
if (validate && (peer_chain != NULL)) {
2206+
sk_X509_pop_free(peer_chain, X509_free);
2207+
}
2208+
#endif
2209+
return retval;
2210+
}
2211+
20852212
/*[clinic input]
20862213
_ssl._SSLSocket.version
20872214
[clinic start generated code]*/
@@ -2984,6 +3111,7 @@ static PyMethodDef PySSLMethods[] = {
29843111
_SSL__SSLSOCKET_GET_CHANNEL_BINDING_METHODDEF
29853112
_SSL__SSLSOCKET_CIPHER_METHODDEF
29863113
_SSL__SSLSOCKET_SHARED_CIPHERS_METHODDEF
3114+
_SSL__SSLSOCKET_GETPEERCERTCHAIN_METHODDEF
29873115
_SSL__SSLSOCKET_VERSION_METHODDEF
29883116
_SSL__SSLSOCKET_SELECTED_NPN_PROTOCOL_METHODDEF
29893117
_SSL__SSLSOCKET_SELECTED_ALPN_PROTOCOL_METHODDEF

Modules/clinic/_ssl.c.h

Lines changed: 51 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)