-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
[WIP] Add SNI support to OpenSSL::SSL::Context::Server. #5755
Conversation
spec/std/openssl/ssl/server.csr
Outdated
Oe8UMl5dCuO/efCPpBy46cYtUhZel0Z0Tak1OycE6M/2asWoz7kgc9P3hdy6v1nZ | ||
wrYe8DyQ3DImnL9JVsUJk7lxewlTzOEWA4AnuianF5WWI8XkhP6v0CQufLtvKPtG | ||
O4TUQWXQy7upOFZWvBdSsNiAU/DAlHakXJ4fhSM= | ||
-----END CERTIFICATE REQUEST----- |
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.
We don't need the CSR in the repo I think.
src/openssl/ssl/context.cr
Outdated
getter ctxbox | ||
getter! sni | ||
|
||
def add_sni(hostname : String, context : 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.
I would go for some slightly more verbose add_sni_hostname
. Documentation would be great too, especially since having to pass a Context::Server
to a Context::Server
quickly gets confusing.
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 how about a variant that takes multiple hostnames? Since at least the bare and the www
hostname in the same certificate is very common.
d738c43
to
a1b181c
Compare
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.
Nice implementation! I noted a bunch of pedantic enhancements to make it perfect.
I have a question thought: why do we have to pass context
to the add_sni_hostname
methods and store it? Isn't the context always self
, or does SNI means we can have multiple contexts? But then, maybe the context should default to self
or nil
(don't bother changing the SSL context)?
One last pedantic: maybe you don't need to box the context, it shall be kept alive long enough for GC to not garbage it, and you may just context.as(Void*)
and ctx = data.as(Server)
(if I'm not mistaken).
src/openssl/ssl/context.cr
Outdated
|
||
property sni_fail_hard | ||
getter ctxbox | ||
getter! sni |
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.
You can group accessors, type and documentation in a single definition, and @sni
is an internal detail (the public API is #add_sni_hostname
):
# :nodoc:
getter! sni : Hash(String, Server)
# ...
property sni_fail_hard = false
Furthermore you shouldn't expose @ctxbox
, it's an unsafe internal detail. It probably doesn't have to be typed either.
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.
Got it. Where should I place developer notes? If I come back to this in six months, I want to know that, say, child contexts shouldn't have @sni set.
src/openssl/ssl/context.cr
Outdated
getter! sni | ||
|
||
def add_sni_hostname(hostname : String, context : Server) | ||
unless @sni |
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 set_tlsext_servername_callback
itself could start with return if @sni
?
src/openssl/ssl/context.cr
Outdated
cb = ->(ssl : LibSSL::SSL, cmd : LibC::Int, ctxptr : Void*) { | ||
ctx = Box(Server).unbox(ctxptr) | ||
if ctx.sni_fail_hard | ||
ret = LibSSL::SSL_TLSEXT_ERR_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.
I'm not sure I understand sni_fail_hard
. If true then it will accept any hostname, which is weird.
Should OK and NOACK be reversed or should OK be something else?
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.
fixed in latest push. I wasn't initializing the contexts correctly throughout the spec, and that coupled with the sni_fail_ahrd issue let tests pass. sni_fail_hard, if set, should immediately throw an error to any SSL client connecting (see any of cloudflares sites).
src/openssl/ssl/context.cr
Outdated
else | ||
nil | ||
end | ||
if hostname |
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 avoid a double check:
if hostname_ptr
hostname = String.new(hostname_ptr)
# ...
end
src/openssl/ssl/context.cr
Outdated
# ctx.add_sni_hostname(["example.com","*.example.com"],sniCtx) | ||
# If you want to require clients to present SNI hostnames, enable sni_fail_hard. | ||
# ctx.sni_fail_hard=true | ||
# Now pass ctx to your OpenSSL server, and clients will be handed the correct certificate for the SNI hostname they pass in. |
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 documentation will be rendered as one big paragraph. A bit a markdown information would help.
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 hope this is replying to the right comment. Regarding having self
as the default context, that's what the current functionality does. SNI just allows different hostnames to be added, and with each new hostname comes a new context that the client will be attached to. I can reexplain this if I've botched the explanation.
src/openssl/ssl/context.cr
Outdated
sni[hostname] = context | ||
end | ||
|
||
def add_sni_hostnames(hostnames : Array(String), context : 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.
Maybe use Enumerable(String)
instead of Array(String)
so the method would accept more collection types.
spec/std/openssl/ssl/sni_spec.cr
Outdated
# run an ssl server with the supplied server context and port | ||
def run_server(q, port, tls = nil) | ||
q.send "start" | ||
tcpServer = TCPServer.new port |
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, use snake_case
(ditto all below) instead of camelCase
.
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. I reread the style guide again. Don't know where I got this idea from.
ret = LibSSL::SSL_TLSEXT_ERR_OK | ||
break | ||
end | ||
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'm not sure these two branches should be the same interface, that is for an interface that asks me for hostnames explicitly I would expect the certificate to only be presented for that exact hostname.
On the other hand being able to just give a bunch of certificates and have the valid hostnames figured out from that is quite useful, so a separate interface for that does make sense to me. That should then clearly document that in case of certificates with overlapping hostnames, say you add one for example.org and example.com and one for example.org and example.net, the first one that matches win.
Finally I think mixing both approaches should also work but still a certificate that was added via the "figure the hostnames from it" interface shouldn't be presented for an explicitly given hostname, likewise an explicitly given hostname's certificate should always win over the implicit one if present.
Now this starts to accumulate quite some logic to the point where I start to wonder whether a Context::Server
subclass makes sense, perhaps with some nice self yielding constructor so you can do something like
server.context = OpenSSL::SSL::Context::SniServer.new do |ctx|
ctx.add_certifcate cert_a, key_a
ctx.add_certificate "example.org", cert_b, key_b
ctx.add_certificate "*.example.org", cert_c, key_c # explicit hostnames with wildcard matching probably warrants its own PR already
ctx.add_certificate "example.net", some_context
ctx.add_certificate another_context # not sure about that one, would need to validate the context has a certificate etc.
end
I don't want to ask you to do all of that now and I am mostly brainstorming here. But if there's some general agreement along those thoughts we should maybe figure out what the minimal thing is we can do here without introducing weird behaviours that we have to break later.
@jhass mentioned ordering. I need to add a note on ordering, at the very least. If someone adds a wildcard hostname, then a specific hostname, the specific is going to win. If someone adds a second duplicate hostname, should it be ignored, or raised? |
I would raise or have the last added one win actually, with a slight but not strong tendency towards the latter. We might need to stop raising once we support adding certificates without explicit hostnames given, in case the certificates overlap. Of course such behaviour needs clear documenation too. |
src/openssl/ssl/context.cr
Outdated
end | ||
ret | ||
} | ||
unless @sni |
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.
Isn't this branch redundant with return if @sni
above?
I think I've handled all of the comments/reviews presented. I'm still having GC issues though, and I don't have a clue how to fix those. If I'm understanding what's happening correctly, the child contexts are never GC'd. |
Closing stale PR. Thank you anyways. |
I'm having GC errors storing sni certificates inside their parent. I've tried making each SNI certificate a WeakRef, but no luck.
I'd love suggestions, because I'm stumped.
Specs should cover all cases, with and without the client providing SNI, and with and without SNI hardfail being set.