Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow accessing a connection's verfied certificate chain #894

Merged
merged 9 commits into from
Aug 5, 2020

Conversation

ShaneHarvey
Copy link
Contributor

@ShaneHarvey ShaneHarvey commented Mar 3, 2020

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.

Motivation: PyMongo is implementing OCSP support and we're using PyOpenSSL to do so (thanks!). As part of the client's OCSP callback we would ideally have access to the verified peer certificate chain to find the peer's issuer cert. This change allows us to do that by exposing the verified peer certificate chain.

References:

Fixes #740.

@ShaneHarvey
Copy link
Contributor Author

ShaneHarvey commented Aug 5, 2020

@alex @reaperhulk will this PR ever be considered? I'd be happy to rebase it.

Copy link
Member

@reaperhulk reaperhulk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First round of review 😄

result = []
for i in range(_lib.sk_X509_num(cert_stack)):
# TODO could incref instead of dup here
cert = _lib.X509_dup(_lib.sk_X509_value(cert_stack, i))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah let's X509_up_ref this instead of duping. Be sure to assert on the result for any call you make (X509_up_ref returns 1 on 1.1.0+ and the refcount in 1.0.2 I believe so it should probably be _openssl_assert(res >= 1).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I also refactored this with get_peer_cert_chain.

return None

cert_stack = _lib.SSL_get_peer_cert_chain(self._ssl)
if cert_stack == _ffi.NULL:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When does this happen? If it should never happen we should do _openssl_assert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL is returned if the peer did not present a certificate: https://www.openssl.org/docs/man1.1.0/man3/SSL_get_peer_cert_chain.html

SSL_get_peer_cert_chain() returns a pointer to STACK_OF(X509) certificates forming the certificate chain sent by the peer. If called on the client side, the stack also contains the peer's certificate; if called on the server side, the peer's certificate must be obtained separately using SSL_get_peer_certificate(3). If the peer did not present a certificate, NULL is returned.

However, we already know that the peer did present a certificate because self.get_peer_certificate() is not None. I replaced this with _openssl_assert(cert_stack != _ffi.NULL).

src/OpenSSL/crypto.py Show resolved Hide resolved
# Note: X509_STORE_CTX_get1_chain returns a deep copy of the chain.
cert_stack = _lib.X509_STORE_CTX_get1_chain(self._store_ctx)
if cert_stack == _ffi.NULL:
# TODO: This is untested.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't trigger this branch use _openssl_assert

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I think _openssl_assert is correct since we already called X509_verify_cert successfully:
https://www.openssl.org/docs/man1.0.2/man3/X509_STORE_CTX_get1_chain.html

X509_STORE_CTX_get1_chain() returns a complete validate chain if a previous call to X509_verify_cert() is successful.

src/OpenSSL/crypto.py Show resolved Hide resolved
chain = _create_certificate_chain()
[(cakey, cacert), (ikey, icert), (skey, scert)] = chain

serverContext = Context(TLSv1_METHOD)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you change all these to SSLv23_METHOD? TLSv1_METHOD won't work in Ubuntu 20.04 (which is what our CI just switched to) because TLS 1.0 is disabled. SSLv23_METHOD is an awful name but will allow up to TLS 1.3.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@reaperhulk
Copy link
Member

For a little context here, we're generally reluctant to add new features to pyOpenSSL because we don't have the time/bandwidth to review things effectively and generally we want people using pyca/cryptography where possible. However, pyca/cryptography has no intention of having a TLS API any time soon so features like this are appropriate to build and review here. Even if we only review when we're feeling exceedingly guilty...

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 pyca#740.
Copy link
Contributor Author

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @reaperhulk. This is ready for another look.

return None

cert_stack = _lib.SSL_get_peer_cert_chain(self._ssl)
if cert_stack == _ffi.NULL:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL is returned if the peer did not present a certificate: https://www.openssl.org/docs/man1.1.0/man3/SSL_get_peer_cert_chain.html

SSL_get_peer_cert_chain() returns a pointer to STACK_OF(X509) certificates forming the certificate chain sent by the peer. If called on the client side, the stack also contains the peer's certificate; if called on the server side, the peer's certificate must be obtained separately using SSL_get_peer_certificate(3). If the peer did not present a certificate, NULL is returned.

However, we already know that the peer did present a certificate because self.get_peer_certificate() is not None. I replaced this with _openssl_assert(cert_stack != _ffi.NULL).

src/OpenSSL/crypto.py Show resolved Hide resolved
chain = _create_certificate_chain()
[(cakey, cacert), (ikey, icert), (skey, scert)] = chain

serverContext = Context(TLSv1_METHOD)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

result = []
for i in range(_lib.sk_X509_num(cert_stack)):
# TODO could incref instead of dup here
cert = _lib.X509_dup(_lib.sk_X509_value(cert_stack, i))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I also refactored this with get_peer_cert_chain.

src/OpenSSL/crypto.py Show resolved Hide resolved
# Note: X509_STORE_CTX_get1_chain returns a deep copy of the chain.
cert_stack = _lib.X509_STORE_CTX_get1_chain(self._store_ctx)
if cert_stack == _ffi.NULL:
# TODO: This is untested.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I think _openssl_assert is correct since we already called X509_verify_cert successfully:
https://www.openssl.org/docs/man1.0.2/man3/X509_STORE_CTX_get1_chain.html

X509_STORE_CTX_get1_chain() returns a complete validate chain if a previous call to X509_verify_cert() is successful.

@reaperhulk
Copy link
Member

Tests are failing right now

@reaperhulk
Copy link
Member

Please also add a CHANGELOG.rst entry for this new feature!

Copy link
Member

@reaperhulk reaperhulk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few final comments + please add a CHANGELOG entry

return None

pystorectx = X509StoreContext(pystore, pycert)
pystorectx._add_chain(cert_stack)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just set _chain directly and remove the setter since it doesn't hold any logic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

# Make the store context available for use after instantiating this
# class by initializing it now. Per testing, subsequent calls to
# :meth:`_init` have no adverse affect.
self._init()

def _add_chain(self, chain):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed once the other comment is addressed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/OpenSSL/crypto.py Show resolved Hide resolved
Copy link
Contributor Author

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a Changelog entry and removed _add_chain.

# Make the store context available for use after instantiating this
# class by initializing it now. Per testing, subsequent calls to
# :meth:`_init` have no adverse affect.
self._init()

def _add_chain(self, chain):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return None

pystorectx = X509StoreContext(pystore, pycert)
pystorectx._add_chain(cert_stack)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@reaperhulk reaperhulk merged commit 33c5499 into pyca:master Aug 5, 2020
@reaperhulk
Copy link
Member

Thanks for working with us!

@ShaneHarvey ShaneHarvey deleted the add_verified_chain branch August 6, 2020 00:28
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 5, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

Retrieve chain after certificate validation - what should API look like?
2 participants