-
-
Notifications
You must be signed in to change notification settings - Fork 263
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
base: master
Are you sure you want to change the base?
Conversation
There was a problem hiding this 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.
pyftpdlib/handlers.py
Outdated
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 |
There was a problem hiding this comment.
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:
.
There was a problem hiding this comment.
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``). |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
pyftpdlib/handlers.py
Outdated
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
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
5737721
to
6b4aae7
Compare
@giampaolo Thanks for your review, I've made changes accordingly and passed CI. BTW, Issue #336 seems to be related to this PR. |
@giampaolo May you kindly review the updated PR? |
docs/api.rst
Outdated
to the server (default ``None``). | ||
|
||
.. versionadded:: 1.5.3 | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
pyftpdlib/handlers.py
Outdated
@@ -3419,6 +3419,8 @@ class TLS_FTPHandler(SSLConnection, FTPHandler): | |||
certfile = None | |||
keyfile = None | |||
ssl_protocol = SSL.SSLv23_METHOD | |||
# client certificate configurable attributes |
There was a problem hiding this comment.
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
pyftpdlib/handlers.py
Outdated
if cls.ssl_options: | ||
cls.ssl_context.set_options(cls.ssl_options) | ||
return cls.ssl_context | ||
def validate_ssl_options(cls): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why was this renamed?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@giampaolo Thanks very much for your review, updated per comments. Please kindly review it again and let me know if further update is necessary. |
@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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.
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.