[WIP] Add SNI support to OpenSSL::SSL::Context::Server.#5755
Conversation
| Oe8UMl5dCuO/efCPpBy46cYtUhZel0Z0Tak1OycE6M/2asWoz7kgc9P3hdy6v1nZ | ||
| wrYe8DyQ3DImnL9JVsUJk7lxewlTzOEWA4AnuianF5WWI8XkhP6v0CQufLtvKPtG | ||
| O4TUQWXQy7upOFZWvBdSsNiAU/DAlHakXJ4fhSM= | ||
| -----END CERTIFICATE REQUEST----- |
There was a problem hiding this comment.
We don't need the CSR in the repo I think.
| getter ctxbox | ||
| getter! sni | ||
|
|
||
| def add_sni(hostname : String, context : Server) |
There was a problem hiding this comment.
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.
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
ysbaddaden
left a comment
There was a problem hiding this comment.
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).
|
|
||
| property sni_fail_hard | ||
| getter ctxbox | ||
| getter! sni |
There was a problem hiding this comment.
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 = falseFurthermore 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.
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.
| getter! sni | ||
|
|
||
| def add_sni_hostname(hostname : String, context : Server) | ||
| unless @sni |
There was a problem hiding this comment.
Maybe set_tlsext_servername_callback itself could start with return if @sni?
| 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.
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.
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).
| else | ||
| nil | ||
| end | ||
| if hostname |
There was a problem hiding this comment.
Maybe avoid a double check:
if hostname_ptr
hostname = String.new(hostname_ptr)
# ...
end| # 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.
The documentation will be rendered as one big paragraph. A bit a markdown information would help.
There was a problem hiding this comment.
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.
| sni[hostname] = context | ||
| end | ||
|
|
||
| def add_sni_hostnames(hostnames : Array(String), context : Server) |
There was a problem hiding this comment.
Maybe use Enumerable(String) instead of Array(String) so the method would accept more collection types.
| # 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.
please, use snake_case (ditto all below) instead of camelCase.
There was a problem hiding this comment.
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.
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.
endI 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. |
| end | ||
| ret | ||
| } | ||
| unless @sni |
There was a problem hiding this comment.
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.