jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more
https://eclipse.dev/jetty
Other
3.87k stars 1.91k forks source link

Client mTLS - How to get hold of the request URL when SSL handshake failed #12519

Closed paulhilliar closed 1 week ago

paulhilliar commented 1 week ago

Jetty Version 12.0.13

Jetty Environment ee10

Java Version 21

Question We have Jetty set up with SslContextFactory.Server.setWantClientAuth(true) in order to get Jetty to ask for client certificates.

But when the SSL validation fails, we need to alert the customer differently depending on which URL was being attempted.

Using connector.addBean(new SslHandshakeListener() { .... is fine to give me the callback that the client cert was rejected, but it doesn't give me the request that was being attempted.

I'm assuming this is because you need to negotiate a connection before the actual request can be made?

So is it possible to either

I realise the thing I'm asking for here isn't the cleanest architecturally. We are migrating from an OpenResty-based solution to Jetty and we manage this in OpenResty/Nginx by hooking into rewrite_by_lua (https://openresty-reference.readthedocs.io/en/latest/Directives/#rewrite_by_lua).

Any help or advice would be much appreciated. If it helps then I could put it into a replication project but there didn't seem much point given that I'm not even sure this is possible in Jetty.

joakime commented 1 week ago

When an SSL handshake fails the HTTP request has not been sent yet. We have no information about the scheme, host/port, path, query unless the HTTP request has been sent and fully parsed.

The client cert validation occurs deep within the JVM, we have no way to get involved in that process. From a Java I/O point of view, the connection failed to be established when the cert fails.

paulhilliar commented 1 week ago

Thanks. Would it be possible by not validating the client certificate up front but doing it later (manually triggered) in a filter?

joakime commented 1 week ago

Certificate validation is done in the JVM's SSLEngine, we cannot validate it later in our own code. It is either a valid certificate and the connection proceeds, followed by the HTTP request. Or the certificate is invalid and the connection fails to be established.

sbordet commented 1 week ago

@paulhilliar an alternative could be to install a TrustManager that accepts all certificates, even if they are invalid.

Then you wait for the request.

Then you perform certificate validation, but to be honest I have not tried and I have no idea if it would be possible to retrieve the cryptographic material using the JDK APIs to perform the validation later.

It may require some work on your part to figure this out.

Just curious, what was the rewrite_by_lua code doing, exactly?

paulhilliar commented 1 week ago

@sbordet thanks for the suggestion. I've made this work in a standalone project. I'll tidy it up and publish it shortly in case it helps others. Overview:

  1. No need for SslHandshakeListener

  2. Set SslContextFactory.Server.setWantClientAuth(true); (grabs client cert if present)

  3. Set SslContextFactory.Server.setNeedClientAuth(false); (if no client cert then establish a connection anyway)

  4. Override the trust manager with a 'trust anything' trust manager (if client cert then trust it no matter what)

  5. SSL Session will be negotiated and any client cert present will be 'trusted' enough to make a SSL connection

  6. Use SecureRequestCustomizer so that the SSL session ends up in the request

  7. Handler gets the SSL session from the request and (if appropriate for that URL) validates it using the 'real' trust manager. If there is no client cert or an untrusted client cert then it gets rejected at this point with 401/connection:close response header.

You can argue that it's a security hole to allow the client to establish an insecure connection at all but, given that different URLs will have different MTLS enforcement (in our product at least), it's unavoidable I think.

sbordet commented 1 week ago

@paulhilliar sounds a reasonable approach.

We won't do this in Jetty, but if it works for you, then great.

If you want to publish a solution, please add a comment with the link to the solution, so that others may benefit from your work.

Thanks!

paulhilliar commented 1 week ago

Thanks for the help here. There's no expectation to do this in Jetty but it's nice to figure out a way to bend the rules to get this done.

I tidied up the Jetty client mtls example project in case it helps others: https://github.com/paulhilliar/jetty-client-mtls-play-area

paulhilliar commented 1 week ago

P.S. @sbordet you asked what is happening in rewrite_by_lua - it seems that ngx.var.uri is available at that point. We then subsequently, from within ssl_certificate_by_lua, call ngx.ssl.verify_client

It's difficult for me to tell because, well, it's OpenResty/Lua and I wasn't around when this bit was written but my best guess is that the client verification it postponed (like we have been trying to do here) until we explicitly call it