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.86k stars 1.91k forks source link

No way to set keystore for JSR 356 websocket clients, needed for SSL client authentication #155

Open jmcc0nn3ll opened 8 years ago

jmcc0nn3ll commented 8 years ago

migrated from Bugzilla #442926 status ASSIGNED severity normal in component websocket for 9.3.x Reported in version 9.2.2 on platform All Assigned to: Joakim Erdfelt

On 2014-08-29 17:11:16 -0400, Stephen McCracken wrote:

I'm trying to write a websocket client that uses the standard JSR 356 APIs and the corresponding jetty implementation in javax-websocket-client-impl (9.2.2.v20140723). Without SSL client authentication, everything works. However, when I set up the jetty server to require client authentication (jetty.ssl.needClientAuth=true and jetty.ssl.wantClientAuth=true in start.d/ssl.ini), the client cannot connect.

If I run the jetty server with -Djavax.net.debug=SSL, the source of the problem seems to be that the client is not providing a certificate chain.

*\ Certificate chain

qtp939427899-19, fatal error: 42: null cert chain javax.net.ssl.SSLHandshakeException: null cert chain

Based on internet searching, this probably means that the client keystore is missing or has the wrong content.

Based on an inspection of the code for the following classes:

  • org.eclipse.jetty.util.ssl.SslContextFactory and
  • org.eclipse.jetty.websocket.jsr356.ClientContainer
  • org.eclipse.jetty.websocket.client.WebSocketClient I do not see a way to configure the client keystore.

I have tried several ways that did not work. Defining the following properties on the client command line did not help: -Djavax.net.ssl.keyStore -Djavax.net.ssl.keyStorePassword -Djetty.keystore -Djetty.keystore.password

I also tried manually configuring the SslContextFactory, but it seems that I can't get access to it until "too late". The following code produces the exception below it.

WebSocketContainer container = ContainerProvider.getWebSocketContainer(); if (container instanceof ClientContainer) { ClientContainer jettyClientContainer = (ClientContainer) container; jettyClientContainer.getClient().getSslContextFactory().setKeyStorePath(keystorePath); jettyClientContainer.getClient().getSslContextFactory().setKeyStorePassword(keystorePassword); }

java.lang.IllegalStateException: Cannot modify configuration when STARTED at org.eclipse.jetty.util.ssl.SslContextFactory.checkNotStarted(SslContextFactory.java:1114) at org.eclipse.jetty.util.ssl.SslContextFactory.setKeyStorePath(SslContextFactory.java:439) at org.example.WsTestClient.main(WsTestClient.java:49)

On 2014-08-29 18:16:51 -0400, Stephen McCracken wrote:

Tyrus claims to pick up a default keystore, but requires non-JSR classes to configure it in code. https://tyrus.java.net/documentation/1.8.2/user-guide.html#d0e1128

On 2014-09-22 15:52:15 -0400, Joakim Erdfelt wrote:

That is absolutely 100% correct. There is no way.

There is an open / unresolved spec bug for this at https://java.net/jira/browse/WEBSOCKET_SPEC-210

On 2015-12-28 17:06:36 -0500, Peter Robbins wrote:

This also impacts when you need to programmatically provide truststore to ClientContainer.

public ClientContainer(WebSocketContainerScope scope)
{
    boolean trustAll = Boolean.getBoolean("org.eclipse.jetty.websocket.jsr356.ssl-trust-all");

    this.scopeDelegate = scope;
    client = new WebSocketClient(scope, new SslContextFactory(trustAll));
    client.setEventDriverFactory(new JsrEventDriverFactory(client.getPolicy()));
    SessionFactory sessionFactory = new JsrSessionFactory(this,this,client);
    client.setSessionFactory(sessionFactory);
    addBean(client);

    this.endpointClientMetadataCache = new ConcurrentHashMap<>();
    this.decoderFactory = new DecoderFactory(this,PrimitiveDecoderMetadataSet.INSTANCE);
    this.encoderFactory = new EncoderFactory(this,PrimitiveEncoderMetadataSet.INSTANCE);

    ShutdownThread.register(this);
}

WebSocketContainerScope has the getSslContextFactory() method that would allow to inject an SslContextFactory using a the proper truststore.

This line overrides that: client = new WebSocketClient(scope, new SslContextFactory(trustAll));

This is seemingly done to consume the org.eclipse.jetty.websocket.jsr356.ssl-trust-all system property. I think that should be consumed somewhere other than the ClientContainer constructor. Perhaps somewhere in SimpleContainerScope?

joakime commented 8 years ago

Hopefully, the next version of JSR356 (javax.websocket) will address this flaw in the API.

mohanrao commented 8 years ago

I agree with @jmcc0nn3ll to provide at least a fix the overloaded constructor in the next version

joakime commented 8 years ago

Since the only sanctioned way to create a JSR356 WebSocket client is to call the javax.websocket.ContainerProvider.getWebSocketContainer() ...

example:

    WebSocketContainer container = ContainerProvider.getWebSocketContainer();
    EchoEndpoint echoer = new EchoEndpoint();
    Session session = container.connectToServer(echoer,URI.create("ws://remote/echo");
    session.getBasicRemote().sendText("Echo");

... there's no opportunity to use a new constructor version, even if it existed.

The functionality you are looking for simply does not exist for the JSR356 API.

It does, however, exist for the Jetty Native Websocket API (which is the original, non-JSR356, more capable, WebSocket API Jetty provides)

pdubs10 commented 8 years ago

I can explain the use case where I ran into this.

I need ajavax.websocket.WebSocketContainer to pass into org.springframework.web.socket.client.standard.StandardWebSocketClient and I can build up a simple org.eclipse.jetty.websocket.jsr356.ClientContainer with a WebSocketContainerScope (SimpleContainerScope) that specifies the SslContextFactory, BUT the constructor does not use the SslContextFactory from the WebSocketContainer scope and instead creates a new one on the third line of the contructor in order to consume the org.eclipse.jetty.websocket.jsr356.ssl-trust-all system property.

public ClientContainer(WebSocketContainerScope scope)
{
    boolean trustAll = Boolean.getBoolean("org.eclipse.jetty.websocket.jsr356.ssl-trust-all");

    this.scopeDelegate = scope;
    client = new WebSocketClient(scope, new SslContextFactory(trustAll));
    client.setEventDriverFactory(new JsrEventDriverFactory(client.getPolicy()));
    SessionFactory sessionFactory = new JsrSessionFactory(this,this,client);
    client.setSessionFactory(sessionFactory);
    addBean(client);

    this.endpointClientMetadataCache = new ConcurrentHashMap<>();
    this.decoderFactory = new DecoderFactory(this,PrimitiveDecoderMetadataSet.INSTANCE);
    this.encoderFactory = new EncoderFactory(this,PrimitiveEncoderMetadataSet.INSTANCE);

    ShutdownThread.register(this);
}

It's more an issue when plugging into other libraries that use JSR356 API than using pure JSR356 API websockets.

joakime commented 7 years ago

Since JSR356 Client uses WebSocketClient, and WebSocketClient uses the internal HttpClient, the decisions being made in Issue #1528 should provide a technique to configure any HttpClient specific features, even on JSR356.

joakime commented 7 years ago

jetty-9.4.x (jetty-9.4.7-SNAPSHOT) and master (jetty-10.x) branches have support for this via configuration of the internal HttpClient now.

bahadirdanisik commented 6 years ago

jetty-9.4.x (jetty-9.4.7-SNAPSHOT) and master (jetty-10.x) branches have support for this via configuration of the internal HttpClient now.

Do you have any examples how to configure client authentication in http client? (For websockets).

joakime commented 6 years ago

The AuthenticationStore is probably were you want to look. https://www.eclipse.org/jetty/documentation/current/http-client-authentication.html

Feel free to open a new issue, show some sample code and what you are attempting to do.

bahadirdanisik commented 6 years ago

Sorry I was not clear. I am looking an example for SSL client authentication. When you set the "needClientAuth" to true on SslContextFactory, client is expected to send its certificate, but as mentioned in this ticket, it is not. As this ticket is already closed, I am looking at how to configure jetty http client to provide a custom ssl context factory or any other way to take the key store into account and send the certificate.

Thanks.

bahadirdanisik commented 6 years ago

Can you please also show me an example, how to configure the jetty http client programatically which is used to make websocket calls?

joakime commented 6 years ago

That is unrelated to this closed issue. Please open a new issue.

bahadirdanisik commented 6 years ago

Thank you joakime, but I will probably create the same ticket as this one. "No way to set keystore for JSR 356 websocket clients, needed for SSL client authentication #155". So, wondering how is this ticket closed? I am looking the solution for this ticket. How do I set the keystore here?

joakime commented 6 years ago

you are asking about behaviors before even the HTTP level. client certificate auth has nothing to do with websocket. once the client certificate auth is valid/verified on the server, then the HTTP protocol begins, which asks to upgrade, then the websocket upgrade can proceed.

bahadirdanisik commented 6 years ago

I don't think you are getting my questions. What I am asking is how to force the http client used internally to send certificate during tls handshake. This was the original issue also reported in this ticket. ("No way to set keystore"). The client is not sending its certificate to the server.

sbordet commented 6 years ago

@bahadirdanisik to force HttpClient to send the certificate you need to set SslContextFactory.needClientAuth=true on the server. It's the server that asks the client to send the certificate.

bahadirdanisik commented 6 years ago

Yes, I know and already set the needClientAuth=true. This is asking client to send the cert, but the client does not send anything as indicated in this ticket. The httpClient used by Jetty does not honor the keystore settings. It does not send the certificate to the server.

sbordet commented 6 years ago

Please pack a reproducible test case and attach it to this issue.

bahadirdanisik commented 5 years ago

Any update for this ticket? Thanks.

sbordet commented 5 years ago

@bahadirdanisik no updates because there is nothing to do.

It is already possible to configure the client and the server so that the client sends to the server a certificate to authenticate itself, and we know that that works: we have tests and other people using this setup.

You have been asked a reproducible case for your issue, but you did not.

horiavmuntean commented 5 years ago

I did not find a way to make a jetty websocket client send a ssl client certificate for auth over websocket to the server (as opposed to a websocket client from the browser that does it to the same server - so the server is configured to ask for client certs)

Please @sbordet can you give any references/examples (you said - we have tests) with a jetty websocket client being able to send a ssl client certificate for auth to a server (any server) ?

Thanks

joakime commented 5 years ago

@horiavmuntean this issue is for the JSR356 WebSocket Client in Jetty (using the javax.websocket.WebSocketContainer API).

What you are asking about ("jetty websocket client") is a different client (org.eclipse.jetty.websocket.client.WebSocketClient API).

Can you open a new issue with what you have attempted so far, some example code on how you setup the WebSocketClient, what results you have (logs are great here), and what you expect.

horiavmuntean commented 5 years ago

Hi @joakime , thanks for clarifications. In the meantime I "solved" the problem by upgrading from 9.2.2 to 9.4.17.

melowe commented 5 years ago

Hi. I've been having the same issue (or at least it looks that way) trying to get SSL working with the jsr356 websockets jetty support. As I've seen in various comments on the many threads on this issue I configure a HttpClient pass in the same SSLContext, SslContextFactory.setSslContext(sslContext) and then new HttpClient(SslContextFactory).

Finding the exact means of addressing this is proving quite laborious. The error I get is

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.validator.PKIXV

Nothing I try seems to deal with this.

The SSLContext we build we use to create jaxrs client so that (at least for rest) works.

The jetty version I"m using is 9.4.20.v20190813 which I thought (from on of the threads) these problems had been fixed.

In case it helps the code looks like this.

final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
final SslContextFactory ssl = new SslContextFactory.Client();
final SSLContext sslContext = //our factory creates the sslcontext;
ssl.setSslContext(sslContext);

HttpClient httpClient = new HttpClient(ssl);
context.setAttribute(HTTPCLIENT_ATTRIBUTE, httpClient);
ServerContainer websocketsContainer = WebSocketServerContainerInitializer.initialize(context);

I see in initialise(context) that the http client is read from the context and then used to eventually create the WebSocketClient. Providing the HttpClient that this doesn't seem to work at all, am I missing something?

Thanks

Mark

sbordet commented 5 years ago

unable to find valid certification path to requested target

@melowe this says that the server sent down to the client a certificate that is either self-signed or the client does not know how to trust - it's a normal TLS failure due to the client TLS configuration being misconfigured.

joakime commented 5 years ago

sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at sun.security.validator.PKIXV

If you are getting the above you are definitely using SSL/TLS for your connection. (it's trying to handshake) You just have a SSL/TLS configuration issue to work out.

joakime commented 5 years ago

@melowe I went ahead and added an example of configuring javax.websocket client for SSL on Jetty (in commit 5bcbe0f9d9ab83c14584b63ef2ffe18579f89f9e)

See directory: javax-websocket-client-impl - /examples/

And the main entry point : SecureWebSocketContainerExample.java

This is just the javax.websocket equivalent of the same code at jetty-client - /examples/SimpleSecureEchoClient.java

melowe commented 5 years ago

thanks joakime.

so I found the issue(s). when the dust settles I see if i can send some patches.

JettyClientContainerProvider doesn't have any direct means of injecting the sslContext. Adding a static reference to the JettyClientContainerProvider and some like

 SimpleContainerScope containerScope = new SimpleContainerScope(WebSocketPolicy.newClientPolicy());

if (sslContext != null) {
            final SslContextFactory ssl = new SslContextFactory.Client();
            ssl.setSslContext(sslContext);
            containerScope.setSslContextFactory(ssl);
}

ClientContainer clientContainer = new ClientContainer(containerScope);

This makes ContainerProvider.getContainer() work. This does create a different issue where the injection via a static function of the sslContext means these cannot be created in start up, but need to be created as there's no way of knowing whether the context has been provided or not.

The previous way of providing the http client as a server attribute to the WebSocketServerContainerInitializer works fine.

The provision of the objects provided in the xml is the part of your example that would make the difference but we're not using xml config so we had to find an alternative. The configurator and the non annotated endpoint seem to make no difference.

joakime commented 5 years ago

Hmm ... SimpleContainerScope is an internal class, and its been removed in Jetty 10 already.

Let me make a new ClientContainer(HttpClient) constructor that can be used to configure the HttpClient side (for ssl, and proxies). But that means you cannot use javax.websocket.ContainerProvider.getWebSocketContainer().

Would this be acceptable?

joakime commented 5 years ago

Opened PR #4019 for new ClientContainer(HttpClient) optional constructor.

It would be used like this ...

https://github.com/eclipse/jetty.project/blob/f692af3d539c63ea455359a5b93537653dd4aba6/jetty-websocket/javax-websocket-client-impl/src/test/java/examples/SecureClientContainerExample.java#L74-L83

melowe commented 5 years ago

So injecting the http client rather than the scope seems fine. But I'm not sure how to deal with the javax.websocket.ContainerProvider you end up with a race condition between when the clients are created and when the addition of the SSLContext is added. I guess some block until initialised, initialisation being JettyClientContainerProvider.configured() or something.. and getContainer() can block with a timeout or something..

joakime commented 5 years ago

See previous https://github.com/eclipse/jetty.project/issues/155#issuecomment-524387906 for example on how PR #4019 would work.

But I'm not sure how to deal with the javax.websocket.ContainerProvider you end up with a race condition between when the clients are created and when the addition of the SSLContext is added.

This is why the XML configuration was provided as an option. Your XML can choose do anything it wants, just as long as it <Configure class="org.eclipse.jetty.client.HttpClient">, go load a ssl configuration from a DB, access a singleton to get your tls configuration, have it use a shared thread pool, etc...

You are correct, you cannot modify the SSL configuration (even the SSLContext) once the javax.websocket.WebSocketContainer is created. The javax.websocket.ContainerProvider.getWebSocketContainer() is a java.util.ServiceLoader that calls JettyClientContainerProvider and should not block or timeout.

You could even have that XML delegate to your own class, block in there if you want to.

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_3.dtd">

<Configure id="myHttpClient" class="org.eclipse.jetty.client.HttpClient">
  <Arg>
    <New class="org.eclipse.jetty.util.ssl.SslContextFactory$Client" />
  </Arg>
  <Call class="examples.CustomHttpClientConfig" name="configure">
    <Arg><Ref refid="myHttpClient"/></Arg>
  </Call>
</Configure>

Where the class looks like ...

package examples;

import org.eclipse.jetty.client.HttpClient;

public class CustomHttpClientConfig
{
    public static void configure(HttpClient httpClient)
    {
        httpClient.getSslContextFactory().setExcludeCipherSuites(); // echo.websocket.org uses WEAK cipher suites
        httpClient.setIdleTimeout(5000);
    }
}

... a race condition between when the clients are created ...

This is a red flag!

You should only use that call once; either once for your JVM, or once per deployed WebApp, as its a heavyweight component. There should never be multiple accesses to javax.websocket.ContainerProvider.getWebSocketContainer(). It will return a new Client container on every call. (This is important to recognize)

melowe commented 5 years ago

So the only means of injecting the sslcontext into the HttpClient (via scope or otherwise) is by using the xml config. The point that the Container should be present and correct when getContainer is called is fair enough. I didn't find anything in the spec regarding how implementations should not block etc, but blocking is less than ideal.

I think we'll need to resolve this but providing our own JettyClientContainerProvider that can load the SSLContext config internally or even a HttpClientFactory that can read the SSLContext.

I've been looking in the jetty code, is there no equivalent of the xml http client config that can be done programmatically?