-
Notifications
You must be signed in to change notification settings - Fork 47
Apply ssl verify mode to the new sni context #1583
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
base: main
Are you sure you want to change the base?
Conversation
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
|
Thanks for reporting this and writing up a fix! Do you think it is possible to cover this scenario in the specs? |
|
I am not too familiar with Crystal nor the codebase but I will give it a try. After all proper PR should contain tests :) |
|
I did not find any easier way to check. Tests are mostly copied from |
|
Thanks for trying. The team will take a look after the holidays. :) Happy new year! 🎆 |
kickster97
left a comment
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.
LGTM, thanks for the contribution! :)
|
|
||
| tcp_server.close | ||
| server_done.receive | ||
| end |
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 think the two tests in this test should be split up to separate tests, to make sure both scenarios always run and keep each test a little shorter
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.
Yeah I agree. I just don't know Crystal well enough to make informed choices. I copied the code from other tests in sni_spec.cr and altered it slightly. I think it was copied from this:
Lines 222 to 315 in 26ded76
| describe "SNI end-to-end" do | |
| it "serves correct certificate based on SNI hostname" do | |
| # Set up SNI manager with different certificates | |
| sni_manager = LavinMQ::SNIManager.new | |
| # Wildcard certificate for *.example.com | |
| wildcard_host = LavinMQ::SNIHost.new("*.example.com") | |
| wildcard_host.tls_cert = "spec/resources/wildcard_example_certificate.pem" | |
| wildcard_host.tls_key = "spec/resources/wildcard_example_key.pem" | |
| sni_manager.add_host(wildcard_host) | |
| # Certificate for foobar.localhost (exact match) | |
| foobar_host = LavinMQ::SNIHost.new("foobar.localhost") | |
| foobar_host.tls_cert = "spec/resources/foobar_localhost_certificate.pem" | |
| foobar_host.tls_key = "spec/resources/foobar_localhost_key.pem" | |
| sni_manager.add_host(foobar_host) | |
| # Default server context (for unmatched hostnames) | |
| default_ctx = OpenSSL::SSL::Context::Server.new | |
| default_ctx.certificate_chain = "spec/resources/server_certificate.pem" | |
| default_ctx.private_key = "spec/resources/server_key.pem" | |
| # Set up SNI callback | |
| default_ctx.set_sni_callback do |hostname| | |
| if sni_host = sni_manager.get_host(hostname) | |
| sni_host.amqp_tls_context | |
| else | |
| nil | |
| end | |
| end | |
| # Start TLS server | |
| tcp_server = TCPServer.new("127.0.0.1", 0) | |
| port = tcp_server.local_address.port | |
| server_done = Channel(Nil).new | |
| spawn do | |
| 3.times do | |
| if client = tcp_server.accept? | |
| begin | |
| ssl_socket = OpenSSL::SSL::Socket::Server.new(client, default_ctx) | |
| ssl_socket.close | |
| rescue | |
| # Ignore handshake errors in server | |
| ensure | |
| client.close | |
| end | |
| end | |
| end | |
| server_done.send(nil) | |
| end | |
| # Helper to create client context that trusts our self-signed certs | |
| create_client_ctx = ->(cert_file : String) { | |
| ctx = OpenSSL::SSL::Context::Client.new | |
| ctx.verify_mode = OpenSSL::SSL::VerifyMode::PEER | |
| ctx.ca_certificates = cert_file | |
| ctx | |
| } | |
| # Test 1: Connect with wildcard hostname (test.example.com) | |
| tcp_client1 = TCPSocket.new("127.0.0.1", port) | |
| client_ctx1 = create_client_ctx.call("spec/resources/wildcard_example_certificate.pem") | |
| begin | |
| ssl_client1 = OpenSSL::SSL::Socket::Client.new(tcp_client1, client_ctx1, hostname: "test.example.com") | |
| ssl_client1.close | |
| ensure | |
| tcp_client1.close | |
| end | |
| # Test 2: Connect with another wildcard subdomain (foo.example.com) | |
| tcp_client2 = TCPSocket.new("127.0.0.1", port) | |
| client_ctx2 = create_client_ctx.call("spec/resources/wildcard_example_certificate.pem") | |
| begin | |
| ssl_client2 = OpenSSL::SSL::Socket::Client.new(tcp_client2, client_ctx2, hostname: "foo.example.com") | |
| ssl_client2.close | |
| ensure | |
| tcp_client2.close | |
| end | |
| # Test 3: Connect with exact match hostname (foobar.localhost) | |
| tcp_client3 = TCPSocket.new("127.0.0.1", port) | |
| client_ctx3 = create_client_ctx.call("spec/resources/foobar_localhost_certificate.pem") | |
| begin | |
| ssl_client3 = OpenSSL::SSL::Socket::Client.new(tcp_client3, client_ctx3, hostname: "foobar.localhost") | |
| ssl_client3.close | |
| ensure | |
| tcp_client3.close | |
| end | |
| tcp_server.close | |
| server_done.receive | |
| end |
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.
👍
Yeah, that test should also be changed to 3 separate tests at some point IMO 🙂
|
Added a couple of comments, besides that I think it looks good! 👍 |
Apparently with SNI the call to
LibSSL.ssl_set_ssl_ctx()does not carry theverify_modefromnew_contextto thesslobject. This causes connections with bad client cert always succeed even if mTLS is configured.This PR fixes the problem by calling
LibSSL.ssl_set_verify()on the SSL connection to set the verify mode after changing the context.Not sure if there is a better way to do this but atleast the fix works for me.
Configuration
I have the following config (domain changed to example.com):
Behavior before patching
Before the patch connection succeeds both without client cert and with a bad client cert.
Connection succeeds with the correct client cert as expected.
Behavior after patching
After applying this patch connection without client cert or with bad client cert fail as expected.
Connections with client cert still work as expected.