Skip to content
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

Closed
wants to merge 9 commits into from

Conversation

bmmcginty
Copy link
Contributor

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.

Oe8UMl5dCuO/efCPpBy46cYtUhZel0Z0Tak1OycE6M/2asWoz7kgc9P3hdy6v1nZ
wrYe8DyQ3DImnL9JVsUJk7lxewlTzOEWA4AnuianF5WWI8XkhP6v0CQufLtvKPtG
O4TUQWXQy7upOFZWvBdSsNiAU/DAlHakXJ4fhSM=
-----END CERTIFICATE REQUEST-----
Copy link
Member

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.

getter ctxbox
getter! sni

def add_sni(hostname : String, context : Server)
Copy link
Member

@jhass jhass Feb 27, 2018

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.

Copy link
Member

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.

Copy link
Contributor

@ysbaddaden ysbaddaden left a 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).


property sni_fail_hard
getter ctxbox
getter! sni
Copy link
Contributor

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.

Copy link
Contributor Author

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.

getter! sni

def add_sni_hostname(hostname : String, context : Server)
unless @sni
Copy link
Contributor

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?

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
Copy link
Contributor

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?

Copy link
Contributor Author

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).

else
nil
end
if hostname
Copy link
Contributor

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

# 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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.

sni[hostname] = context
end

def add_sni_hostnames(hostnames : Array(String), context : Server)
Copy link
Contributor

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.

# 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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Member

@jhass jhass Feb 27, 2018

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.

@bmmcginty
Copy link
Contributor Author

@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?

@jhass
Copy link
Member

jhass commented Feb 28, 2018

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
Copy link
Contributor

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?

@bmmcginty
Copy link
Contributor Author

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.

@straight-shoota
Copy link
Member

Closing stale PR.

Thank you anyways.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants