dependency-track icon indicating copy to clipboard operation
dependency-track copied to clipboard

4.12.0 can no longer log in - WeakKeyException

Open black-snow opened this issue 1 year ago • 7 comments

Current Behavior

The upgrade to 4.12.0 effectively bricked my DT instance for I can no longer log in. It 500s with:

2024-10-16 07:57:44,031 ERROR [GlobalExceptionHandler] Uncaught internal server error [requestId=a470b97a-7f76-4dbb-87e3-bc516362c4ee]
io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 160 bits which is not secure enough for any JWT HMAC-SHA algorithm.  The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size).  Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.  See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.
	at io.jsonwebtoken.security.Keys.hmacShaKeyFor(Keys.java:83)
	at alpine.server.auth.JsonWebToken.<init>(JsonWebToken.java:86)
	at alpine.server.auth.JsonWebToken.<init>(JsonWebToken.java:97)
	at org.dependencytrack.resources.v1.UserResource.validateCredentials(UserResource.java:112)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
	at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52)
	at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher$1.run(AbstractJavaResourceMethodDispatcher.java:146)
	at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.invoke(AbstractJavaResourceMethodDispatcher.java:189)
	at org.glassfish.jersey.server.model.internal.JavaResourceMethodDispatcherProvider$ResponseOutInvoker.doDispatch(JavaResourceMethodDispatcherProvider.java:176)
	at org.glassfish.jersey.server.model.internal.AbstractJavaResourceMethodDispatcher.dispatch(AbstractJavaResourceMethodDispatcher.java:93)
	at org.glassfish.jersey.server.model.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:478)
	at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:400)
	at org.glassfish.jersey.server.model.ResourceMethodInvoker.apply(ResourceMethodInvoker.java:81)
	at org.glassfish.jersey.server.ServerRuntime$1.run(ServerRuntime.java:274)
	at org.glassfish.jersey.internal.Errors$1.call(Errors.java:248)
	at org.glassfish.jersey.internal.Errors$1.call(Errors.java:244)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:292)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:274)
	at org.glassfish.jersey.internal.Errors.process(Errors.java:244)
	at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:266)
	at org.glassfish.jersey.server.ServerRuntime.process(ServerRuntime.java:253)
	at org.glassfish.jersey.server.ApplicationHandler.handle(ApplicationHandler.java:696)
	at org.glassfish.jersey.servlet.WebComponent.serviceImpl(WebComponent.java:397)
	at org.glassfish.jersey.servlet.WebComponent.service(WebComponent.java:349)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:358)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:312)
	at org.glassfish.jersey.servlet.ServletContainer.service(ServletContainer.java:205)
	at org.eclipse.jetty.ee10.servlet.ServletHolder$NotAsync.service(ServletHolder.java:1379)
	at org.eclipse.jetty.ee10.servlet.ServletHolder.handle(ServletHolder.java:736)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1614)
	at alpine.server.filters.ContentSecurityPolicyFilter.doFilter(ContentSecurityPolicyFilter.java:225)
	at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586)
	at alpine.server.filters.ClickjackingFilter.doFilter(ClickjackingFilter.java:93)
	at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586)
	at alpine.server.filters.WhitelistUrlFilter.doFilter(WhitelistUrlFilter.java:166)
	at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:208)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1586)
	at org.eclipse.jetty.ee10.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1547)
	at org.eclipse.jetty.ee10.servlet.ServletChannel.dispatch(ServletChannel.java:824)
	at org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:436)
	at org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:464)
	at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:575)
	at org.eclipse.jetty.ee10.servlet.SessionHandler.handle(SessionHandler.java:703)
	at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1060)
	at org.eclipse.jetty.server.Server.handle(Server.java:181)
	at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:661)
	at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:406)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99)
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:478)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:441)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:293)
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:201)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:311)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164)
	at java.base/java.lang.Thread.run(Unknown Source)

That happens with OID as well as with username+password.

The error message ain't that helpful and if some security default was raised it should rather warn about it than just stop working. Seems to be an issue with jsonwebtoken that just bubbles up.

Steps to Reproduce

Not quite sure what key this is about so I'm not sure how to reproduce.

  1. Install 4.11.x
  2. Set up secret according to https://docs.dependencytrack.org/getting-started/configuration/#secret-key
  3. Set up data dir according to https://docs.dependencytrack.org/getting-started/data-directory/
  4. Upgrade to 4.12.0
  5. Try to log in

Expected Behavior

I should still be able to log in.

Dependency-Track Version

4.12.0

Dependency-Track Distribution

Container Image

Database Server

PostgreSQL

Database Server Version

13.x

Browser

Mozilla Firefox

Checklist

black-snow avatar Oct 16 '24 08:10 black-snow

I suppose that https://docs.dependencytrack.org/getting-started/configuration/#secret-key is the issue and that 32 is now "too short". But I cannot just set a new key, can I?

/edit: Luckily, a downgrade to 4.11 worked.

black-snow avatar Oct 16 '24 08:10 black-snow

openssl rand 32 generates a secure random of 32 bytes, which is 256 bits, which in turn should satisfy the library's check.

Can you double-check if your key is indeed 32 bytes long?

In any case I'll check if we can do something to make this a warning log rather than an exception.

nscuro avatar Oct 16 '24 10:10 nscuro

I cannot reproduce this with a key generated by openssl rand 32:

openssl rand 32 > secret.key

docker run \
    -v "$(pwd)/secret.key:/var/run/secrets/secret.key:ro" \
    -e 'ALPINE_SECRET_KEY_PATH=/var/run/secrets/secret.key' \
    -p '127.0.0.1:8080:8080' dependencytrack/bundled:4.12.0

Change password of initial admin user:

curl 'http://localhost:8080/api/v1/user/forceChangePassword' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'username=admin&password=admin&newPassword=admin123&confirmPassword=admin123'

Login as admin, yielding the properly signed JWT:

curl 'http://localhost:8080/api/v1/user/login' \
   -H 'Content-Type: application/x-www-form-urlencoded' \
   -d 'username=admin&password=admin123'

eyJhbGciOiJIUzI1NiJ9.eyJ...

nscuro avatar Oct 16 '24 11:10 nscuro

Thanks for the quick reply @nscuro !

Weird things are going on. I dropped into the api container and was looking for /secret/secret.key (value of ALPINE_SECRET_KEY_PATH) and found ... nothing.

A quick find yielded: /secret/..2024_10_16_08_08_54.351889796/.secret-key

That's pretty weird. Does DT do some kind of backup when upgrading to 4.12.x?

Disregarding the weird location the actual contents match with my actual key which seems to be ... 20 bytes long? Did the docs change? I'm close to 100% sure I copy pasted the generation. Is there a sane way to ... update the key?

black-snow avatar Oct 16 '24 19:10 black-snow

The odd location is due to how k8s mounts ConfigMaps and Secrets - it mounts them to those seemingly random paths and then symlinks those to your desired path. IIRC that's what enables updating of mounts at runtime.

Our docs always mentioned a key length of 32. It's also the length of the key generated by DT in case you don't provide a custom one.

The good news is that changing the key is not overly painful. You can just generate a new one. What will happen is:

  1. Any active login session will expire since JWT validation will fail. Users merely need to login again to get a JWT signed with the new key.
  2. If you have configured any API tokens for external services (OSS Index, GitHub, VulnDB etc.), you need to re-enter them so they get encrypted with the new key.

nscuro avatar Oct 16 '24 19:10 nscuro

Argh, thanks. Still weird tho, the /secret/secret.key was a symlink but it pointed into the void and nothing pointed to the weird path. Gonna check again tomorrow.

Thanks for the clarification regarding changing the key! Should probably go into the docs, too (unless I've missed it). I'll give it a shot tomorrow and report back.


Still, changing the error to a warning seems reasonable. Erroring out on the very first run would make sense as to prevent the user from doing potentially stupid things, but on consecutive boots it should just warn. If there is a cheap way to check at that point if it's a fresh install it might make sense to differentiate between the two use cases, imho.

black-snow avatar Oct 16 '24 20:10 black-snow

Unfortunately there's no way to prevent the JWT library from failing if the provided key is too short. It has added multiple checks that cannot be bypassed, and likely for good reason.

We could pad the key to reach the >=256bit requirement, but I fear that would lead to users never getting to know that their key is too short. Also, in cases similar like yours, it would invalidate existing encrypted values too, so there's no real benefit.

nscuro avatar Oct 17 '24 15:10 nscuro

Thank you very much for looking into this. I regenerated the key and everything seems fine.

black-snow avatar Oct 21 '24 10:10 black-snow

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

github-actions[bot] avatar Nov 21 '24 10:11 github-actions[bot]