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

Add support for TLS/SSL mutual authentication #428

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

zuohaocheng
Copy link

Add support in the TLS_FTPHandler to check a client certificate. This
type of support strengthens the security between the client and the
server, only allowing clients with a valid certificate to connect to the
server.

Updated the api.rst file with the two new configurable options to make
client authentication work.

Note: This is an improvement of #396 as the original committer has moved on to another team.

Copy link
Owner

@giampaolo giampaolo left a comment

Choose a reason for hiding this comment

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

Thanks a lot for putting this together. I added a couple of comments inline but overall this looks good (and thanks for the unit tests).

Just one thing: this means the client can only authenticate if it provides a certfile, correct?

Last: please also update HISTORY and CREDITS files.

from OpenSSL.SSL import SESS_CACHE_OFF
from OpenSSL.SSL import VERIFY_CLIENT_ONCE
from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT
from OpenSSL.SSL import VERIFY_PEER
Copy link
Owner

Choose a reason for hiding this comment

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

I wouldn't do this at import time. If one of these constants is not available for some reason the whole FTPS support won't be available. Better to do SSL.* later, when if self.client_certfile is not None:.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

docs/api.rst Outdated

The path of the certificate to check the client certificate against. (default ``None``).
Copy link
Owner

Choose a reason for hiding this comment

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

How about adding "...only allowing clients with a valid certificate to connect to the server"?

Copy link
Owner

Choose a reason for hiding this comment

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

Also, please add:

     .. versionadded:: 1.5.3

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for advices and done.

# Does not follow naming convention of other tests because this
# CANNOT be run in the same test suite with test_functional_ssl.
# The test parallelism causes SSL errors when there should be none
# Please run these tests separately
Copy link
Owner

Choose a reason for hiding this comment

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

Mmmm this worries me. What does it mean exactly? That if one of these tests fail also the next ones related to SSL will fail as well?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for reminding. I found out the root cause is TLS_FTPHandler.get_ssl_context is a static method, thus it would get messed up in test cases. I changed it to non-static and the issue is gone, so these tests are merged back to test_functional_ssl.py.

cls.verify_certs_callback)
cls.ssl_context.load_verify_locations(cls.client_certfile)
cls.ssl_context.set_session_cache_mode(SESS_CACHE_OFF)
cls.ssl_options = cls.ssl_options | OP_NO_TICKET
Copy link
Owner

Choose a reason for hiding this comment

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

It would be nice to add a comment explaining what SESS_CACHE_OFF and OP_NO_TICKET do.

self.log("Bad client certificate detected.")
else:
self.log("Client certificate is valid.")
return ok
Copy link
Owner

Choose a reason for hiding this comment

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

What happens if the client submits an invalid cert from the server standpoint? I suppose the connection gets automatically closed due to a SSL.Error exception during the SSL handshake, correct? We already have a callback method for such an event, handle_failed_ssl_handshake. Does it get called?

Copy link
Author

@zuohaocheng zuohaocheng Jul 17, 2017

Choose a reason for hiding this comment

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

It's true that handle_failed_ssl_handshake will be called when client certificate is not provided. Meanwhile, self.log("Bad client certificate detected.") will still be called if client does provide a certificate, but it could not pass the check with server one.

The actual result can be seen from test cases test_auth_client_nocert and test_auth_client_badcert.

bplost and others added 3 commits July 17, 2017 11:11
Add support in the TLS_FTPHandler to check a client certificate.  This
type of support strengthens the security between the client and the
server, only allowing clients with a valid certificate to connect to the
server.

Updated the api.rst file with the two new configurable options to make
client authentication work
@zuohaocheng zuohaocheng force-pushed the master branch 4 times, most recently from 5737721 to 6b4aae7 Compare July 17, 2017 08:20
@zuohaocheng
Copy link
Author

@giampaolo Thanks for your review, I've made changes accordingly and passed CI.

BTW, Issue #336 seems to be related to this PR.

@zuohaocheng
Copy link
Author

@giampaolo May you kindly review the updated PR?

docs/api.rst Outdated
to the server (default ``None``).

.. versionadded:: 1.5.3

Copy link
Owner

Choose a reason for hiding this comment

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

This is not entirely clear. To be cristal clear, does it mean the client must have the exact same certificate? If so, I would recommend this wording:

The path to a file which contains a certificate to be used to 
identify the client. If specified, the client should is supposed to
use the same certificate in order to connect. 

Copy link
Author

@zuohaocheng zuohaocheng Jul 25, 2017

Choose a reason for hiding this comment

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

Thanks for advice and reworded.

The certificates client side and server side need are the basically same, but for client side, private key is needed. To my understanding, it's a bit similar to a "reversed" SSL/TLS certificate validation.

@@ -3419,6 +3419,8 @@ class TLS_FTPHandler(SSLConnection, FTPHandler):
certfile = None
keyfile = None
ssl_protocol = SSL.SSLv23_METHOD
# client certificate configurable attributes
Copy link
Owner

Choose a reason for hiding this comment

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

this line is probably useless

if cls.ssl_options:
cls.ssl_context.set_options(cls.ssl_options)
return cls.ssl_context
def validate_ssl_options(cls):
Copy link
Owner

Choose a reason for hiding this comment

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

why was this renamed?

Copy link
Author

Choose a reason for hiding this comment

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

The static method here does only validation on startup, so I rename it to validate. get_ssl_context, which does the whole setup, is still there. Only thing different is, it is not a static method any more.

Copy link
Owner

Choose a reason for hiding this comment

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

Why not put everything into get_ssl_context then or rename it to _validate_ssl_context? Later on you do:

ctx = TLS_FTPHandler.validate_ssl_options()

...which is a bit weird

if use_client_cert:
self.handler.client_certfile = CLIENT_CERTFILE
else:
self.handler.client_certfile = None
Copy link
Owner

Choose a reason for hiding this comment

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

the else block is probably unnecessary as client_certfile already defaults to None, no?

Copy link
Author

Choose a reason for hiding this comment

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

Hmm not really in this case. client_certfile is a static member, so it won't be reset if the class is instantiated multiple times. It's quite an unlikely use case in production code, but it does matter in unit tests.

@zuohaocheng
Copy link
Author

@giampaolo Thanks very much for your review, updated per comments. Please kindly review it again and let me know if further update is necessary.

@zuohaocheng
Copy link
Author

zuohaocheng commented Jul 27, 2017

@giampaolo Updated, and fixed a test failure due to latest version of python/openssl 3.5 & 3.6, which is not related to the PR.

the client. If specified, only clients with a valid certificate are able
to connect to the server (default ``None``).

.. versionadded:: 1.5.3
Copy link
Owner

Choose a reason for hiding this comment

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

Shouldn't this be "clients with THE SAME certificate"?

cls.ssl_options = cls.ssl_options | OP_NO_TICKET
if cls.ssl_options:
ssl_context.set_options(cls.ssl_options)
return ssl_context
Copy link
Owner

Choose a reason for hiding this comment

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

Sorry but I'm finding it difficult to review this code. Please just add the new functionality to the existent get_ssl_context method, without changing or refactoring anything else. If you/we find out it can be refactored in different methods and made it better let's just do it in another PR, but for now I suggest to keep all changes to the bare minimum.

finally:
TLS_FTPHandler.ssl_context = None
# self.assertFalse(opts & SSL.OP_NO_SSLv3)
# self.assertFalse(opts & SSL.OP_NO_COMPRESSION)
Copy link
Owner

Choose a reason for hiding this comment

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

Same here (keep changes to the bare minimum and refactor in another PR)


def setUp(self):
self.server = FTPSServer(use_client_cert=True)
self.server.start()
Copy link
Owner

Choose a reason for hiding this comment

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

Please do self.client = None and if self.client is not None: ... in the tearDown method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants