-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Turn on TLS client certificate validation by default, expose OpenSSL::SSL::Context in HTTP::Client #2689
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
Conversation
src/openssl/ssl/context.cr
Outdated
|
|
||
| def initialize | ||
| @handle = LibSSL.ssl_ctx_new(LibSSL.sslv23_method) | ||
| self.verify_mode = OpenSSL::SSL::VerifyMode::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.
@ysbaddaden did you understand what this does in server mode? In particular I wonder if setting this in server mode will cause prompts in browsers etc.
SSL_VERIFY_PEER
Server mode: the server sends a client certificate request to the client. The certificate returned (if any) is checked. If the verification process
fails, the TLS/SSL handshake is immediately terminated with an alert message containing the reason for the verification failure.
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.
Maybe for specific configurations where both the client and server share a certificate?
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.
Oh no, it is for the server validating the certificate of the client during TLS client authentication. I just wonder if setting it already makes it mandatory or prompts the client to provide a certificate. The doc is clear that it does give the client the opportunity to send a certificate if this is set.
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.
Maybe we should only set the verify mode from HTTP::Client (and other protocols)?
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 do would like very much to centralize it, it's a good default to have it enabled. So first of all we should test with #2671 whether it's actually an issue at all.
If it is, how about a Context.new_for_server / Context.new_for_client?
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.
Alright, the behavior in browsers is that it does prompt for a client certificate but does require it to proceed (one can cancel). I'll go with the constructor approach then.
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.
Maybe Context.new(:client) and Context.new(:server) instead of distinct constructors?
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.
That would be compile time vs runtime errors on typos, no?
691b32a to
440289e
Compare
|
Alright, rebased against #2671 |
|
Looks good overall. I'm just not fond of the |
|
Apart from that: :+1+ |
Yet, at some latter point we might want to configure different default ciphersuites and things like that there. The issue is that passing the wrong thing to |
|
But does the compiler has any mean to detect that? Both cases (symbol or factories) return the same type and behave exactly the same for the compiler. |
|
My point is that it's a lot |
|
We could make it mandatory? @asterite what do you think? Do you have a better solution? |
|
If an OpenSSL::SSL::Socket created as a client requires an OpenSSL::SSL::Context created as a client, we could maybe have subclasses of each (ClientSocket, ClientContext), and then to create a ClientSocket you would have to pass a ClientContext, and passing a ServerContext would not compile. Then Socket and Context would be abstract. That's the only way the compiler could help. @jhass Sorry about |
|
@asterite Yes that came to my mind too. Note that requires is not exactly right, it'll behave potentially unintentionally. The question however is, what's your opinion on the preferred route we should take here? |
|
I see that context.verify_mode = OpenSSL::SSL::VerifyMode::PEERCan the socket check this and raise a runtime error if creating a client socket and the context's verify mode is not peer? Or are there additional checks to do? It this can be done, then it's simpler to introduce a class hierarchy. This error will be raised as soon as the code is used wrong, so it's almost as good as a compile-time check. |
|
My current plans go into setting the default ciphersuite and enabled protocol versions there too, though likely for both server and client. But in the future we might want to have different defaults in those settings for server and client too. We can't do sanity checks in the socket, as passing a context with those defaults changed to any other value is a perfectly valid usecase. It should just happen very explicitly and intentionally by the user. |
|
So |
|
Yes, I'll toy a bit with a inheritance based approach, that already seems to cleanup |
|
Alright, that was easier than expected and I think I like it more. See the last commit for the changes, all others remained unchanged. |
src/openssl/ssl/socket.cr
Outdated
| abstract class OpenSSL::SSL::Socket | ||
| class Client < Socket | ||
| def initialize(io, context : Context::Client = Context::Client.new, sync_close : Bool = false, hostname : String? = nil) | ||
| super(io, context.as(Context), sync_close) |
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.
@asterite btw isn't having to cast here a bug?
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 don't get any error if I leave that case out. What's a code to reproduce the bug?
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.
Mmh, must have been a fluke in some intermediate state.
|
Unrelated, but this in master now fails: require "http/client"
puts HTTP::Client.get("https://www.google.com").bodyMaybe one of the previous changes to SSL broke this? |
|
Seems to happen after merging #2676 |
|
After more thinking, I'm not sure OpenSSL::SSL::Context should assume anything about what will use the SSL context. Especially setting ciphers, but even setting the verify mode. This should be controlled by whatever will use the context, namely HTTP::Server, HTTP::Client, LDAP, IMAP... I think there should be a single 👍 for distinguishing the Server and Client SSL sockets! |
|
Sorry but I'm not following that thinking at all. Say I write an IRC client that supports connecting to TLS servers, I'll do |
|
@ysbaddaden I would really like to see this in, as I'm not understanding it could you please elaborate your stance as to why the defaults for a generic encryption protocol, which TLS is, it's independent of the application protocol it encrypts, should be specific to said application protocol and not reside with the TLS implementation? Put differently, why does a HTTP server need different defaults than a IMAP4 server? Or a IRC client than a SIP client? Sure, the specific endpoints on the other side you want to support might need differing configurations, but that's the application developers concern and doesn't warrant to make finding good defaults a concern of the protocol library developer. What am I missing here that could allow us to find a compromise? |
|
Indeed Yet, I don't think it's a good idea to create an artificial difference between a client context and a server context when there isn't any in OpenSSL; and it would be bad to enforce a specific client or server context class at the SSL socket level, since it will necessarily come preconfigured, and one would have to reset everything before reconfiguring the context to its needs... Ruby for example only sets the Maybe I'm just picky, or I doesn't know SSL/TLS enough and I believe that different protocols may need different SSL/TLS modes, options, ciphers, ECDH / DH configurations, when there is no such thing. What about:
Sorry for being so stubborn 🙇 |
And the current approach does that very cleanly by just instantiating the right context.
I use the same TLS config for all services on my server, HTTP, SMTP, IMAP4, POP3, Sieve, XMPP, probably something I forgot. I just can't imagine a reason as to why good defaults should differ. The point is that it should be hard to undo the defaults, because we should consider any non default less secure, else we shouldn't pick it as a default.
That approach makes it hard to get a context with good defaults and change just what you need. It means that either context = OpenSSL::SSL::Context.new
context.certificate_chain = chain
context.private_key = key
client = HTTP::Client.new(host, tls: context)will be less secure than client = HTTP::Client.new(host)Or that each and every user of Now going back to While Ruby has a quite extensive OpenSSL interface that's easy to map to the manpages, I think we can still vastly improve over it and it shouldn't be our single role model. What about a Sorry for being stubborn too, but to be honest I'm trying to come up with arguments for my position while your three arguments I could find so far, and that just might be my failure at reading(!), are:
|
|
Oh and btw I agree on the long to type stuff, but I would like to keep that out of the discussion for this PR, adding convenience methods at other places is always something we can easily do later, but shouldn't influence the core design. |
Good to know! Huum, what about making class OpenSSL::SSL::Context
DEFAULT_METHOD = LibSSL.sslv23_method
DEFAULT_CIPHERS = %w( ... ).join(' ')
DEFAULT_OPTIONS = LibSSL::Options::ALL | LibSSL::Options::NO_SSLV2 | LibSSL::Options::NO_SSLV3
DEFAULT_MODES = LibSSL::Modes::AUTO_RETRY | LibSSL::Modes::RELEASE_BUFFERS
private def initialize(method : LibSSL::Method)
end
# Returns a secure context configuration for client SSL sockets.
#
# The context is configured per the intermediate recommendation
# from https://wiki.mozilla.org/Security/Server_Side_TLS with peer
# certificate verification enabled.
def self.client(method = DEFAULT_METHOD, host = nil)
end
# Returns a secure context configuration for server SSL sockets.
#
# The context is configured per the intermediate recommendation
# from https://wiki.mozilla.org/Security/Server_Side_TLS
def self.server(method = DEFAULT_METHOD)
end
# Returns a blank, insecure, context.
#
# A blank context is highly INSECURE by default and enables
# deprecated and broken protocols and ciphers. The context MUST
# be configured properly before use. Please consider the default
# `client` or `server` contexts instead that are preconfigured
# with better defaults.
def self.insecure(method = DEFAULT_METHOD)
end
end |
|
That's where I was going with |
|
I'm willing to compromise too! All your changes are great, and we're almost at a good TLS integration in Crystal. Definitely more secure and less buggy! I guess
|
# Server
context = OpenSSL::SSL::Context::Server.new # we still want SSLv2/3 disabled as a default
context.ciphers = MODERN_CIPHERS
Server.new(tls: context)
# Client
context = OpenSSL::SSL::Context::Client.new # we still want SSLv2/3 disabled as a default and peer verification enabled
context.ciphers = MODERN_CIPHERS
Client.new(tls: context)
# Server
# I also don't want to replicate good defaults in case my users didn't supply any configuration for an option
context = OpenSSL::SSL::Context::Server.new
context.ciphers = config.ciphers if config.ciphers?
context.verify_mode = config.verify_mode if config.verify_mode?
context.methods = config.methods if config.methods?
context.ca_certificates = config.ca_certificates if config.ca_certificates?
# .... you get the pattern
Server.new(tls: context)
# Client
# I also don't want to replicate good defaults in case my users didn't supply any configuration for an option
context = OpenSSL::SSL::Context::Client.new
context.ciphers = config.ciphers if config.ciphers?
context.verify_mode = config.verify_mode if config.verify_mode?
context.methods = config.methods if config.methods?
context.ca_certificates = config.ca_certificates if config.ca_certificates?
# .... you get the pattern
Client.new(tls: context)
# Only makes sense in the client role
CIPHERS.each do |cipher|
context = OpenSSL::SSL::Context::Client.insecure
# ...
begin
OpenSSL::SSL::Socket::Client.new(sock, context: context, sync_close: false)
rescue e : OpenSSL::SSL::Error
# ...
end
endMakes sense? |
While global defaults are charming, there's no sane way to copy a SSLContext. This too easily leads to exposing the default context in connection specific APIs and unwanted side effects through that.
Also allow setting CA paths in context and add error checking there
|
Added |
OpenSSL::SSL::Socket::Server and OpenSSL::SSL::Socket::Client
|
👌 Then all that's left is to remove |
|
Yes, I'd like to do that in a follow up PR, this is already big enough :) |
| end | ||
|
|
||
| protected def self.insecure(method : LibSSL::SSLMethod) | ||
| obj = allocate |
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 would avoid using allocate, as it basically bypasses some compiler type checks. How about an initialize method with a required named argument insecure? We would just ignore that parameter's value, it's just a marker to allow an extra overload.
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 tried that but couldn't get it to work without getting public. Feel free to checkout the branch and try to find a way, the requirement is that the caller cannot pass it but has to use insecure and the context_spec.cr file still passes.
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.
Hm, yeah, we'd need to duplicate that initialize in the subclasses. I tried that but I think I found a bug in the compiler, let's keep it like this for now :-)
|
So, ready from my side @asterite @ysbaddaden |
|
Done! |
Don't merge before #2671, includes redundant work that's better done there.This is basically an alternative to #2343. Fixes #2073.
This does three things:
HTTP::Client#sslaOpenSSL::SSL:Context::Clientas well as allssloptions inHTTP::Clientalso take one.OpenSSL::SSL::Context.default, since it's not easy to copy a context, we should avoid global mutable defaults. More reasoning in the commit message.This does not yet setup a strong cipher suite as #2671 does for the server side, which would fix #2674's second issue. We can look into what we can reuse from there in a follow up PR. It also does not yet set a sane default for enabled protocol versions, for largely the same reasons.