Skip to content

Commit 33c5499

Browse files
authored
Allow accessing a connection's verfied certificate chain (#894)
* Allow accessing a connection's verfied certificate chain Add X509StoreContext.get_verified_chain using X509_STORE_CTX_get1_chain. Add Connection.get_verified_chain using SSL_get0_verified_chain if available (ie OpenSSL 1.1+) and X509StoreContext.get_verified_chain otherwise. Fixes #740. * TLSv1_METHOD -> SSLv23_METHOD * Use X509_up_ref instead of X509_dup * Add _openssl_assert where appropriate * SSL_get_peer_cert_chain should not be null * Reformat with black * Fix <OpenSSL.crypto.X509 object at 0x7fdbb59e8050> != <OpenSSL.crypto.X509 object at 0x7fdbb59daad0> * Add Changelog entry * Remove _add_chain
1 parent bb971ae commit 33c5499

File tree

5 files changed

+190
-11
lines changed

5 files changed

+190
-11
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ Deprecations:
2121

2222
- Deprecated ``OpenSSL.crypto.loads_pkcs7`` and ``OpenSSL.crypto.loads_pkcs12``.
2323

24-
*none*
25-
26-
2724
Changes:
2825
^^^^^^^^
2926

3027
- Added ``Context.set_keylog_callback`` to log key material.
3128
`#910 <https://github.com/pyca/pyopenssl/pull/910>`_
29+
- Added ``OpenSSL.SSL.Connection.get_verified_chain`` to retrieve the
30+
verified certificate chain of the peer.
31+
`#894 <https://github.com/pyca/pyopenssl/pull/894>`_.
3232

3333

3434
19.1.0 (2019-11-18)

src/OpenSSL/SSL.py

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
X509Name,
2929
X509,
3030
X509Store,
31+
X509StoreContext,
3132
)
3233

3334
__all__ = [
@@ -2126,6 +2127,22 @@ def get_peer_certificate(self):
21262127
return X509._from_raw_x509_ptr(cert)
21272128
return None
21282129

2130+
@staticmethod
2131+
def _cert_stack_to_list(cert_stack):
2132+
"""
2133+
Internal helper to convert a STACK_OF(X509) to a list of X509
2134+
instances.
2135+
"""
2136+
result = []
2137+
for i in range(_lib.sk_X509_num(cert_stack)):
2138+
cert = _lib.sk_X509_value(cert_stack, i)
2139+
_openssl_assert(cert != _ffi.NULL)
2140+
res = _lib.X509_up_ref(cert)
2141+
_openssl_assert(res >= 1)
2142+
pycert = X509._from_raw_x509_ptr(cert)
2143+
result.append(pycert)
2144+
return result
2145+
21292146
def get_peer_cert_chain(self):
21302147
"""
21312148
Retrieve the other side's certificate (if any)
@@ -2137,13 +2154,43 @@ def get_peer_cert_chain(self):
21372154
if cert_stack == _ffi.NULL:
21382155
return None
21392156

2140-
result = []
2141-
for i in range(_lib.sk_X509_num(cert_stack)):
2142-
# TODO could incref instead of dup here
2143-
cert = _lib.X509_dup(_lib.sk_X509_value(cert_stack, i))
2144-
pycert = X509._from_raw_x509_ptr(cert)
2145-
result.append(pycert)
2146-
return result
2157+
return self._cert_stack_to_list(cert_stack)
2158+
2159+
def get_verified_chain(self):
2160+
"""
2161+
Retrieve the verified certificate chain of the peer including the
2162+
peer's end entity certificate. It must be called after a session has
2163+
been successfully established. If peer verification was not successful
2164+
the chain may be incomplete, invalid, or None.
2165+
2166+
:return: A list of X509 instances giving the peer's verified
2167+
certificate chain, or None if it does not have one.
2168+
2169+
.. versionadded:: 20.0
2170+
"""
2171+
if hasattr(_lib, "SSL_get0_verified_chain"):
2172+
# OpenSSL 1.1+
2173+
cert_stack = _lib.SSL_get0_verified_chain(self._ssl)
2174+
if cert_stack == _ffi.NULL:
2175+
return None
2176+
2177+
return self._cert_stack_to_list(cert_stack)
2178+
2179+
pycert = self.get_peer_certificate()
2180+
if pycert is None:
2181+
return None
2182+
2183+
# Should never be NULL because the peer presented a certificate.
2184+
cert_stack = _lib.SSL_get_peer_cert_chain(self._ssl)
2185+
_openssl_assert(cert_stack != _ffi.NULL)
2186+
2187+
pystore = self._context.get_cert_store()
2188+
if pystore is None:
2189+
return None
2190+
2191+
pystorectx = X509StoreContext(pystore, pycert)
2192+
pystorectx._chain = cert_stack
2193+
return pystorectx.get_verified_chain()
21472194

21482195
def want_read(self):
21492196
"""

src/OpenSSL/crypto.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,7 @@ def __init__(self, store, certificate):
17121712
self._store_ctx = _ffi.gc(store_ctx, _lib.X509_STORE_CTX_free)
17131713
self._store = store
17141714
self._cert = certificate
1715+
self._chain = _ffi.NULL
17151716
# Make the store context available for use after instantiating this
17161717
# class by initializing it now. Per testing, subsequent calls to
17171718
# :meth:`_init` have no adverse affect.
@@ -1725,7 +1726,7 @@ def _init(self):
17251726
:meth:`_cleanup` will leak memory.
17261727
"""
17271728
ret = _lib.X509_STORE_CTX_init(
1728-
self._store_ctx, self._store._store, self._cert._x509, _ffi.NULL
1729+
self._store_ctx, self._store._store, self._cert._x509, self._chain
17291730
)
17301731
if ret <= 0:
17311732
_raise_current_error()
@@ -1797,6 +1798,45 @@ def verify_certificate(self):
17971798
if ret <= 0:
17981799
raise self._exception_from_context()
17991800

1801+
def get_verified_chain(self):
1802+
"""
1803+
Verify a certificate in a context and return the complete validated
1804+
chain.
1805+
1806+
:raises X509StoreContextError: If an error occurred when validating a
1807+
certificate in the context. Sets ``certificate`` attribute to
1808+
indicate which certificate caused the error.
1809+
1810+
.. versionadded:: 20.0
1811+
"""
1812+
# Always re-initialize the store context in case
1813+
# :meth:`verify_certificate` is called multiple times.
1814+
#
1815+
# :meth:`_init` is called in :meth:`__init__` so _cleanup is called
1816+
# before _init to ensure memory is not leaked.
1817+
self._cleanup()
1818+
self._init()
1819+
ret = _lib.X509_verify_cert(self._store_ctx)
1820+
if ret <= 0:
1821+
self._cleanup()
1822+
raise self._exception_from_context()
1823+
1824+
# Note: X509_STORE_CTX_get1_chain returns a deep copy of the chain.
1825+
cert_stack = _lib.X509_STORE_CTX_get1_chain(self._store_ctx)
1826+
_openssl_assert(cert_stack != _ffi.NULL)
1827+
1828+
result = []
1829+
for i in range(_lib.sk_X509_num(cert_stack)):
1830+
cert = _lib.sk_X509_value(cert_stack, i)
1831+
_openssl_assert(cert != _ffi.NULL)
1832+
pycert = X509._from_raw_x509_ptr(cert)
1833+
result.append(pycert)
1834+
1835+
# Free the stack but not the members which are freed by the X509 class.
1836+
_lib.sk_X509_free(cert_stack)
1837+
self._cleanup()
1838+
return result
1839+
18001840

18011841
def load_certificate(type, buffer):
18021842
"""

tests/test_crypto.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3849,6 +3849,41 @@ def test_verify_with_time(self):
38493849

38503850
assert exc.value.args[0][2] == "certificate has expired"
38513851

3852+
def test_get_verified_chain(self):
3853+
"""
3854+
`get_verified_chain` returns the verified chain.
3855+
"""
3856+
store = X509Store()
3857+
store.add_cert(self.root_cert)
3858+
store.add_cert(self.intermediate_cert)
3859+
store_ctx = X509StoreContext(store, self.intermediate_server_cert)
3860+
chain = store_ctx.get_verified_chain()
3861+
assert len(chain) == 3
3862+
intermediate_subject = self.intermediate_server_cert.get_subject()
3863+
assert chain[0].get_subject() == intermediate_subject
3864+
assert chain[1].get_subject() == self.intermediate_cert.get_subject()
3865+
assert chain[2].get_subject() == self.root_cert.get_subject()
3866+
# Test reuse
3867+
chain = store_ctx.get_verified_chain()
3868+
assert len(chain) == 3
3869+
assert chain[0].get_subject() == intermediate_subject
3870+
assert chain[1].get_subject() == self.intermediate_cert.get_subject()
3871+
assert chain[2].get_subject() == self.root_cert.get_subject()
3872+
3873+
def test_get_verified_chain_invalid_chain_no_root(self):
3874+
"""
3875+
`get_verified_chain` raises error when cert verification fails.
3876+
"""
3877+
store = X509Store()
3878+
store.add_cert(self.intermediate_cert)
3879+
store_ctx = X509StoreContext(store, self.intermediate_server_cert)
3880+
3881+
with pytest.raises(X509StoreContextError) as exc:
3882+
store_ctx.get_verified_chain()
3883+
3884+
assert exc.value.args[0][2] == "unable to get issuer certificate"
3885+
assert exc.value.certificate.get_subject().CN == "intermediate"
3886+
38523887

38533888
class TestSignVerify(object):
38543889
"""

tests/test_ssl.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,63 @@ def test_get_peer_cert_chain_none(self):
24452445
interact_in_memory(client, server)
24462446
assert None is server.get_peer_cert_chain()
24472447

2448+
def test_get_verified_chain(self):
2449+
"""
2450+
`Connection.get_verified_chain` returns a list of certificates
2451+
which the connected server returned for the certification verification.
2452+
"""
2453+
chain = _create_certificate_chain()
2454+
[(cakey, cacert), (ikey, icert), (skey, scert)] = chain
2455+
2456+
serverContext = Context(SSLv23_METHOD)
2457+
serverContext.use_privatekey(skey)
2458+
serverContext.use_certificate(scert)
2459+
serverContext.add_extra_chain_cert(icert)
2460+
serverContext.add_extra_chain_cert(cacert)
2461+
server = Connection(serverContext, None)
2462+
server.set_accept_state()
2463+
2464+
# Create the client
2465+
clientContext = Context(SSLv23_METHOD)
2466+
# cacert is self-signed so the client must trust it for verification
2467+
# to succeed.
2468+
clientContext.get_cert_store().add_cert(cacert)
2469+
clientContext.set_verify(VERIFY_PEER, verify_cb)
2470+
client = Connection(clientContext, None)
2471+
client.set_connect_state()
2472+
2473+
interact_in_memory(client, server)
2474+
2475+
chain = client.get_verified_chain()
2476+
assert len(chain) == 3
2477+
assert "Server Certificate" == chain[0].get_subject().CN
2478+
assert "Intermediate Certificate" == chain[1].get_subject().CN
2479+
assert "Authority Certificate" == chain[2].get_subject().CN
2480+
2481+
def test_get_verified_chain_none(self):
2482+
"""
2483+
`Connection.get_verified_chain` returns `None` if the peer sends
2484+
no certificate chain.
2485+
"""
2486+
ctx = Context(SSLv23_METHOD)
2487+
ctx.use_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem))
2488+
ctx.use_certificate(load_certificate(FILETYPE_PEM, server_cert_pem))
2489+
server = Connection(ctx, None)
2490+
server.set_accept_state()
2491+
client = Connection(Context(SSLv23_METHOD), None)
2492+
client.set_connect_state()
2493+
interact_in_memory(client, server)
2494+
assert None is server.get_verified_chain()
2495+
2496+
def test_get_verified_chain_unconnected(self):
2497+
"""
2498+
`Connection.get_verified_chain` returns `None` when used with an object
2499+
which has not been connected.
2500+
"""
2501+
ctx = Context(SSLv23_METHOD)
2502+
server = Connection(ctx, None)
2503+
assert None is server.get_verified_chain()
2504+
24482505
def test_get_session_unconnected(self):
24492506
"""
24502507
`Connection.get_session` returns `None` when used with an object

0 commit comments

Comments
 (0)