Skip to content

Commit 83ef230

Browse files
orosamSandor Oroszi
andauthored
Allow using additional untrusted certificates for chain building in X509StoreContext (#948)
The additional certificates provided in the new `chain` parameter will be untrusted but may be used to build the chain. This makes it easier to validate a certificate against a store which contains only root ca certificates, and the intermediates come from e.g. the same untrusted source as the certificate to be verified. Co-authored-by: Sandor Oroszi <[email protected]>
1 parent 43c9776 commit 83ef230

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ Deprecations:
2424
Changes:
2525
^^^^^^^^
2626

27+
- Added a new optional ``chain`` parameter to ``OpenSSL.crypto.X509StoreContext()``
28+
where additional untrusted certificates can be specified to help chain building.
29+
`#948 <https://github.com/pyca/pyopenssl/pull/948>`_
2730
- Added ``OpenSSL.crypto.X509Store.load_locations`` to set trusted
2831
certificate file bundles and/or directories for verification.
2932
`#943 <https://github.com/pyca/pyopenssl/pull/943>`_

src/OpenSSL/crypto.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1750,22 +1750,54 @@ class X509StoreContext(object):
17501750
collected.
17511751
:ivar _store: See the ``store`` ``__init__`` parameter.
17521752
:ivar _cert: See the ``certificate`` ``__init__`` parameter.
1753+
:ivar _chain: See the ``chain`` ``__init__`` parameter.
17531754
:param X509Store store: The certificates which will be trusted for the
17541755
purposes of any verifications.
17551756
:param X509 certificate: The certificate to be verified.
1757+
:param chain: List of untrusted certificates that may be used for building
1758+
the certificate chain. May be ``None``.
1759+
:type chain: :class:`list` of :class:`X509`
17561760
"""
17571761

1758-
def __init__(self, store, certificate):
1762+
def __init__(self, store, certificate, chain=None):
17591763
store_ctx = _lib.X509_STORE_CTX_new()
17601764
self._store_ctx = _ffi.gc(store_ctx, _lib.X509_STORE_CTX_free)
17611765
self._store = store
17621766
self._cert = certificate
1763-
self._chain = _ffi.NULL
1767+
self._chain = self._build_certificate_stack(chain)
17641768
# Make the store context available for use after instantiating this
17651769
# class by initializing it now. Per testing, subsequent calls to
17661770
# :meth:`_init` have no adverse affect.
17671771
self._init()
17681772

1773+
@staticmethod
1774+
def _build_certificate_stack(certificates):
1775+
def cleanup(s):
1776+
# Equivalent to sk_X509_pop_free, but we don't
1777+
# currently have a CFFI binding for that available
1778+
for i in range(_lib.sk_X509_num(s)):
1779+
x = _lib.sk_X509_value(s, i)
1780+
_lib.X509_free(x)
1781+
_lib.sk_X509_free(s)
1782+
1783+
if certificates is None or len(certificates) == 0:
1784+
return _ffi.NULL
1785+
1786+
stack = _lib.sk_X509_new_null()
1787+
_openssl_assert(stack != _ffi.NULL)
1788+
stack = _ffi.gc(stack, cleanup)
1789+
1790+
for cert in certificates:
1791+
if not isinstance(cert, X509):
1792+
raise TypeError("One of the elements is not an X509 instance")
1793+
1794+
_openssl_assert(_lib.X509_up_ref(cert._x509) > 0)
1795+
if _lib.sk_X509_push(stack, cert._x509) <= 0:
1796+
_lib.X509_free(cert._x509)
1797+
_raise_current_error()
1798+
1799+
return stack
1800+
17691801
def _init(self):
17701802
"""
17711803
Set up the store context for a subsequent verification operation.

tests/test_crypto.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3824,6 +3824,145 @@ def test_reuse(self):
38243824
assert store_ctx.verify_certificate() is None
38253825
assert store_ctx.verify_certificate() is None
38263826

3827+
@pytest.mark.parametrize(
3828+
"root_cert, chain, verified_cert",
3829+
[
3830+
pytest.param(
3831+
root_cert,
3832+
[intermediate_cert],
3833+
intermediate_server_cert,
3834+
id="intermediate in chain",
3835+
),
3836+
pytest.param(
3837+
root_cert,
3838+
[],
3839+
intermediate_cert,
3840+
id="empty chain",
3841+
),
3842+
pytest.param(
3843+
root_cert,
3844+
[root_cert, intermediate_server_cert, intermediate_cert],
3845+
intermediate_server_cert,
3846+
id="extra certs in chain",
3847+
),
3848+
],
3849+
)
3850+
def test_verify_success_with_chain(self, root_cert, chain, verified_cert):
3851+
store = X509Store()
3852+
store.add_cert(root_cert)
3853+
store_ctx = X509StoreContext(store, verified_cert, chain=chain)
3854+
assert store_ctx.verify_certificate() is None
3855+
3856+
def test_valid_untrusted_chain_reuse(self):
3857+
"""
3858+
`verify_certificate` using an untrusted chain can be called multiple
3859+
times with the same ``X509StoreContext`` instance to produce the same
3860+
result.
3861+
"""
3862+
store = X509Store()
3863+
store.add_cert(self.root_cert)
3864+
chain = [self.intermediate_cert]
3865+
3866+
store_ctx = X509StoreContext(
3867+
store, self.intermediate_server_cert, chain=chain
3868+
)
3869+
assert store_ctx.verify_certificate() is None
3870+
assert store_ctx.verify_certificate() is None
3871+
3872+
def test_chain_reference(self):
3873+
"""
3874+
``X509StoreContext`` properly keeps references to the untrusted chain
3875+
certificates.
3876+
"""
3877+
store = X509Store()
3878+
store.add_cert(self.root_cert)
3879+
chain = [load_certificate(FILETYPE_PEM, intermediate_cert_pem)]
3880+
3881+
store_ctx = X509StoreContext(
3882+
store, self.intermediate_server_cert, chain=chain
3883+
)
3884+
3885+
del chain
3886+
assert store_ctx.verify_certificate() is None
3887+
3888+
@pytest.mark.parametrize(
3889+
"root_cert, chain, verified_cert",
3890+
[
3891+
pytest.param(
3892+
root_cert,
3893+
[],
3894+
intermediate_server_cert,
3895+
id="intermediate missing",
3896+
),
3897+
pytest.param(
3898+
None,
3899+
[intermediate_cert],
3900+
intermediate_server_cert,
3901+
id="no trusted root",
3902+
),
3903+
pytest.param(
3904+
None,
3905+
[root_cert, intermediate_cert],
3906+
intermediate_server_cert,
3907+
id="untrusted root, full chain is available",
3908+
),
3909+
pytest.param(
3910+
intermediate_cert,
3911+
[root_cert, intermediate_cert],
3912+
intermediate_server_cert,
3913+
id="untrusted root, intermediate is trusted and in chain",
3914+
),
3915+
],
3916+
)
3917+
def test_verify_fail_with_chain(self, root_cert, chain, verified_cert):
3918+
store = X509Store()
3919+
if root_cert:
3920+
store.add_cert(root_cert)
3921+
3922+
store_ctx = X509StoreContext(store, verified_cert, chain=chain)
3923+
3924+
with pytest.raises(X509StoreContextError):
3925+
store_ctx.verify_certificate()
3926+
3927+
@pytest.mark.parametrize(
3928+
"chain, expected_error",
3929+
[
3930+
pytest.param(
3931+
[intermediate_cert, "This is not a certificate"],
3932+
TypeError,
3933+
id="non-certificate in chain",
3934+
),
3935+
pytest.param(
3936+
42,
3937+
TypeError,
3938+
id="non-list chain",
3939+
),
3940+
],
3941+
)
3942+
def test_untrusted_chain_wrong_args(self, chain, expected_error):
3943+
"""
3944+
Creating ``X509StoreContext`` with wrong chain raises an exception.
3945+
"""
3946+
store = X509Store()
3947+
store.add_cert(self.root_cert)
3948+
3949+
with pytest.raises(expected_error):
3950+
X509StoreContext(store, self.intermediate_server_cert, chain=chain)
3951+
3952+
def test_failure_building_untrusted_chain_raises(self, monkeypatch):
3953+
"""
3954+
Creating ``X509StoreContext`` raises ``OpenSSL.crypto.Error`` when
3955+
the underlying lib fails to add the certificate to the stack.
3956+
"""
3957+
monkeypatch.setattr(_lib, "sk_X509_push", lambda _stack, _x509: -1)
3958+
3959+
store = X509Store()
3960+
store.add_cert(self.root_cert)
3961+
chain = [self.intermediate_cert]
3962+
3963+
with pytest.raises(Error):
3964+
X509StoreContext(store, self.intermediate_server_cert, chain=chain)
3965+
38273966
def test_trusted_self_signed(self):
38283967
"""
38293968
`verify_certificate` returns ``None`` when called with a self-signed

0 commit comments

Comments
 (0)