Skip to content

Commit 2ef7dfb

Browse files
tuupolacarlhoerbergclaude
authored
Copy SSL options to new sni contexts (#1583)
Apparently with SNI the call to `LibSSL.ssl_set_ssl_ctx()` does not carry the `verify_mode` from `new_context` to the `ssl` object. 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. More options are now explicitly copied: ``` // 1. Verify mode and callback (you already know this) SSL_set_verify // 2. Verify depth SSL_set_verify_depth // 3. Client CA list (for requesting client certificates) SSL_set_client_CA_list // 4. Options flags SSL_clear_options SSL_set_options ``` --------- Co-authored-by: Carl Hörberg <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 0ec59e7 commit 2ef7dfb

3 files changed

Lines changed: 176 additions & 0 deletions

File tree

spec/sni_spec.cr

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require "./spec_helper"
2+
require "../src/stdlib/openssl_on_server_name"
23

34
describe LavinMQ::SNIHost do
45
it "creates TLS contexts with default settings" do
@@ -312,4 +313,111 @@ describe "SNI end-to-end" do
312313
tcp_server.close
313314
server_done.receive
314315
end
316+
317+
it "rejects connection without client cert when mTLS is enabled via SNI" do
318+
sni_manager = LavinMQ::SNIManager.new
319+
mtls_host = LavinMQ::SNIHost.new("mtls.localhost")
320+
mtls_host.tls_cert = "spec/resources/server_certificate.pem"
321+
mtls_host.tls_key = "spec/resources/server_key.pem"
322+
mtls_host.tls_verify_peer = true
323+
mtls_host.tls_ca_cert = "spec/resources/ca_certificate.pem"
324+
sni_manager.add_host(mtls_host)
325+
326+
mtls_host.amqp_tls_context.verify_mode.should eq(OpenSSL::SSL::VerifyMode::PEER | OpenSSL::SSL::VerifyMode::FAIL_IF_NO_PEER_CERT)
327+
328+
default_ctx = OpenSSL::SSL::Context::Server.new
329+
default_ctx.verify_mode = OpenSSL::SSL::VerifyMode::NONE
330+
default_ctx.certificate_chain = "spec/resources/server_certificate.pem"
331+
default_ctx.private_key = "spec/resources/server_key.pem"
332+
333+
default_ctx.on_server_name do |hostname|
334+
sni_manager.get_host(hostname).try(&.amqp_tls_context)
335+
end
336+
337+
tcp_server = TCPServer.new("127.0.0.1", 0)
338+
port = tcp_server.local_address.port
339+
340+
server_done = Channel(Nil).new
341+
342+
spawn do
343+
if client = tcp_server.accept?
344+
begin
345+
ssl_socket = OpenSSL::SSL::Socket::Server.new(client, default_ctx)
346+
ssl_socket.close
347+
rescue
348+
ensure
349+
client.close
350+
end
351+
end
352+
server_done.send(nil)
353+
end
354+
355+
tcp_client = TCPSocket.new("127.0.0.1", port)
356+
client_ctx = OpenSSL::SSL::Context::Client.new
357+
client_ctx.verify_mode = OpenSSL::SSL::VerifyMode::NONE
358+
begin
359+
expect_raises(Exception) do
360+
ssl_client = OpenSSL::SSL::Socket::Client.new(tcp_client, client_ctx, hostname: "mtls.localhost")
361+
ssl_client.gets
362+
end
363+
ensure
364+
tcp_client.close
365+
end
366+
367+
tcp_server.close
368+
server_done.receive
369+
end
370+
371+
it "accepts connection with valid client cert when mTLS is enabled via SNI" do
372+
sni_manager = LavinMQ::SNIManager.new
373+
mtls_host = LavinMQ::SNIHost.new("mtls.localhost")
374+
mtls_host.tls_cert = "spec/resources/server_certificate.pem"
375+
mtls_host.tls_key = "spec/resources/server_key.pem"
376+
mtls_host.tls_verify_peer = true
377+
mtls_host.tls_ca_cert = "spec/resources/ca_certificate.pem"
378+
sni_manager.add_host(mtls_host)
379+
380+
default_ctx = OpenSSL::SSL::Context::Server.new
381+
default_ctx.verify_mode = OpenSSL::SSL::VerifyMode::NONE
382+
default_ctx.certificate_chain = "spec/resources/server_certificate.pem"
383+
default_ctx.private_key = "spec/resources/server_key.pem"
384+
385+
default_ctx.on_server_name do |hostname|
386+
sni_manager.get_host(hostname).try(&.amqp_tls_context)
387+
end
388+
389+
tcp_server = TCPServer.new("127.0.0.1", 0)
390+
port = tcp_server.local_address.port
391+
392+
server_done = Channel(Nil).new
393+
394+
spawn do
395+
if client = tcp_server.accept?
396+
begin
397+
ssl_socket = OpenSSL::SSL::Socket::Server.new(client, default_ctx)
398+
ssl_socket.close
399+
rescue
400+
ensure
401+
client.close
402+
end
403+
end
404+
server_done.send(nil)
405+
end
406+
407+
tcp_client = TCPSocket.new("127.0.0.1", port)
408+
client_ctx = OpenSSL::SSL::Context::Client.new
409+
client_ctx.verify_mode = OpenSSL::SSL::VerifyMode::NONE
410+
client_ctx.certificate_chain = "spec/resources/client_certificate.pem"
411+
client_ctx.private_key = "spec/resources/client_key.pem"
412+
begin
413+
ssl_client = OpenSSL::SSL::Socket::Client.new(tcp_client, client_ctx, hostname: "mtls.localhost")
414+
ssl_client.gets
415+
ssl_client.close
416+
ensure
417+
tcp_client.close
418+
end
419+
420+
tcp_server.close
421+
server_done.receive
422+
end
315423
end

src/lavinmq/launcher.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require "./pidfile"
1010
require "./etcd"
1111
require "./clustering/controller"
1212
require "./standalone_runner"
13+
require "../stdlib/openssl_on_server_name"
1314

1415
module LavinMQ
1516
class Launcher
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Monkey-patch OpenSSL::SSL::Context::Server#on_server_name to copy
2+
# settings that SSL_set_SSL_CTX does not propagate to the SSL connection:
3+
# verify mode, verify depth, client CA list, and options.
4+
# Without this, mTLS and per-host TLS settings break when switching
5+
# contexts via SNI.
6+
7+
require "openssl"
8+
9+
lib LibSSL
10+
fun ssl_set_verify = SSL_set_verify(ssl : SSL, mode : LibC::Int, callback : Void*) : Void
11+
fun ssl_set_verify_depth = SSL_set_verify_depth(ssl : SSL, depth : LibC::Int) : Void
12+
fun ssl_ctx_get_verify_depth = SSL_CTX_get_verify_depth(ctx : SSLContext) : LibC::Int
13+
fun ssl_ctx_get_client_ca_list = SSL_CTX_get_client_CA_list(ctx : SSLContext) : Void*
14+
fun ssl_dup_ca_list = SSL_dup_CA_list(list : Void*) : Void*
15+
fun ssl_set_client_ca_list = SSL_set_client_CA_list(ssl : SSL, list : Void*) : Void
16+
fun ssl_get_options = SSL_get_options(ssl : SSL) : ULong
17+
fun ssl_set_options = SSL_set_options(ssl : SSL, options : ULong) : ULong
18+
fun ssl_clear_options = SSL_clear_options(ssl : SSL, options : ULong) : ULong
19+
end
20+
21+
class OpenSSL::SSL::Context::Server
22+
@[Experimental]
23+
def on_server_name(&block : String -> OpenSSL::SSL::Context::Server?)
24+
c_callback = Proc(LibSSL::SSL, LibC::Int*, Void*, LibC::Int).new do |ssl, alert_ptr, arg|
25+
servername_ptr = LibSSL.ssl_get_servername(ssl, LibSSL::TLSExt::NAMETYPE_host_name)
26+
if servername_ptr.null?
27+
next LibSSL::SSL_TLSEXT_ERR_OK
28+
end
29+
30+
begin
31+
hostname = String.new(servername_ptr)
32+
33+
callback = Box(typeof(block)).unbox(arg)
34+
new_context = callback.call(hostname)
35+
36+
if new_context
37+
new_ctx = new_context.to_unsafe
38+
LibSSL.ssl_set_ssl_ctx(ssl, new_ctx)
39+
40+
# SSL_set_SSL_CTX does not copy these settings:
41+
verify_mode = LibSSL.ssl_ctx_get_verify_mode(new_ctx).to_i
42+
LibSSL.ssl_set_verify(ssl, verify_mode, nil)
43+
LibSSL.ssl_set_verify_depth(ssl, LibSSL.ssl_ctx_get_verify_depth(new_ctx))
44+
45+
ca_list = LibSSL.ssl_ctx_get_client_ca_list(new_ctx)
46+
unless ca_list.null?
47+
LibSSL.ssl_set_client_ca_list(ssl, LibSSL.ssl_dup_ca_list(ca_list))
48+
end
49+
50+
LibSSL.ssl_clear_options(ssl, LibSSL.ssl_get_options(ssl))
51+
LibSSL.ssl_set_options(ssl, LibSSL.ssl_ctx_get_options(new_ctx))
52+
end
53+
54+
LibSSL::SSL_TLSEXT_ERR_OK
55+
rescue
56+
alert_ptr.value = LibSSL::SSL_AD_INTERNAL_ERROR
57+
LibSSL::SSL_TLSEXT_ERR_ALERT_FATAL
58+
end
59+
end
60+
61+
callback_box = Box.box(block)
62+
@sni_callback_box = callback_box
63+
64+
LibSSL.ssl_ctx_callback_ctrl(@handle, LibSSL::SSL_CTRL_SET_TLSEXT_SERVERNAME_CB, c_callback.unsafe_as(Proc(Void)))
65+
LibSSL.ssl_ctx_ctrl(@handle, LibSSL::SSL_CTRL_SET_TLSEXT_SERVERNAME_ARG, 0, callback_box)
66+
end
67+
end

0 commit comments

Comments
 (0)