shred / acme4j

Java client for ACME (Let's Encrypt)
https://acme4j.shredzone.org
Apache License 2.0
509 stars 93 forks source link

acme4j 3.x wrongly validates pebble certificate #160

Closed nsheridan closed 3 weeks ago

nsheridan commented 4 weeks ago

I'm updating a dependency on acme4j from pre-3.x to 3.3 and I've encountered an issue where the http client rejects the certificate presented by pebble. This worked with the previous version of acme4j, maybe an artifact of the switch to the java.net.http client?

The full exception follows:

org.shredzone.acme4j.exception.AcmeNetworkException: Network error
    at org.shredzone.acme4j.connector.DefaultConnection.sendRequest(DefaultConnection.java:182) ~[acme4j-client-3.3.1.jar:?]
    at org.shredzone.acme4j.provider.AbstractAcmeProvider.directory(AbstractAcmeProvider.java:65) ~[acme4j-client-3.3.1.jar:?]
    at org.shredzone.acme4j.Session.readDirectory(Session.java:348) ~[acme4j-client-3.3.1.jar:?]
    at org.shredzone.acme4j.Session.resourceUrlOptional(Session.java:252) ~[acme4j-client-3.3.1.jar:?]
    at org.shredzone.acme4j.Session.resourceUrl(Session.java:238) ~[acme4j-client-3.3.1.jar:?]
    at org.shredzone.acme4j.AccountBuilder.createLogin(AccountBuilder.java:272) ~[acme4j-client-3.3.1.jar:?]
    at org.shredzone.acme4j.AccountBuilder.create(AccountBuilder.java:249) ~[acme4j-client-3.3.1.jar:?]
    ... 
    at java.lang.Thread.run(Thread.java:829) [?:?]
Caused by: java.io.IOException: No subject alternative DNS name matching pebble.cert-pebble.svc.stage.kubernetes found.
    at jdk.internal.net.http.HttpClientImpl.send(HttpClientImpl.java:565) ~[java.net.http:?]
    at jdk.internal.net.http.HttpClientFacade.send(HttpClientFacade.java:119) ~[java.net.http:?]
    at org.shredzone.acme4j.connector.DefaultConnection.sendRequest(DefaultConnection.java:347) ~[acme4j-client-3.3.1.jar:?]
    at org.shredzone.acme4j.connector.DefaultConnection.sendRequest(DefaultConnection.java:164) ~[acme4j-client-3.3.1.jar:?]
    ... 32 more
Caused by: javax.net.ssl.SSLHandshakeException: No subject alternative DNS name matching pebble.cert-pebble.svc.stage.kubernetes found.
    at sun.security.ssl.Alert.createSSLException(Alert.java:131) ~[?:?]
    at sun.security.ssl.TransportContext.fatal(TransportContext.java:353) ~[?:?]
    at sun.security.ssl.TransportContext.fatal(TransportContext.java:296) ~[?:?]
    at sun.security.ssl.TransportContext.fatal(TransportContext.java:291) ~[?:?]
    at sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1357) ~[?:?]
    at sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1232) ~[?:?]
    at sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1175) ~[?:?]
    at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392) ~[?:?]
    at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:443) ~[?:?]
    at sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1074) ~[?:?]
    at sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1061) ~[?:?]
    at java.security.AccessController.doPrivileged(Native Method) ~[?:?]
    at sun.security.ssl.SSLEngineImpl$DelegatedTask.run(SSLEngineImpl.java:1008) ~[?:?]
    at java.util.ArrayList.forEach(ArrayList.java:1541) ~[?:?]
    at jdk.internal.net.http.common.SSLFlowDelegate.lambda$executeTasks$3(SSLFlowDelegate.java:1073) ~[java.net.http:?]
    at jdk.internal.net.http.HttpClientImpl$DelegatingExecutor.execute(HttpClientImpl.java:153) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate.executeTasks(SSLFlowDelegate.java:1068) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate.doHandshake(SSLFlowDelegate.java:1034) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:442) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:264) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$SynchronizedRestartableTask.run(SequentialScheduler.java:175) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:147) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$TryEndDeferredCompleter.complete(SequentialScheduler.java:315) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198) ~[java.net.http:?]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[?:?]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[?:?]
    ... 1 more
Caused by: java.security.cert.CertificateException: No subject alternative DNS name matching pebble.cert-pebble.svc.stage.kubernetes found.
    at sun.security.util.HostnameChecker.matchDNS(HostnameChecker.java:212) ~[?:?]
    at sun.security.util.HostnameChecker.match(HostnameChecker.java:103) ~[?:?]
    at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:455) ~[?:?]
    at sun.security.ssl.X509TrustManagerImpl.checkIdentity(X509TrustManagerImpl.java:415) ~[?:?]
    at sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:283) ~[?:?]
    at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:141) ~[?:?]
    at sun.security.ssl.CertificateMessage$T13CertificateConsumer.checkServerCerts(CertificateMessage.java:1335) ~[?:?]
    at sun.security.ssl.CertificateMessage$T13CertificateConsumer.onConsumeCertificate(CertificateMessage.java:1232) ~[?:?]
    at sun.security.ssl.CertificateMessage$T13CertificateConsumer.consume(CertificateMessage.java:1175) ~[?:?]
    at sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:392) ~[?:?]
    at sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:443) ~[?:?]
    at sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1074) ~[?:?]
    at sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1061) ~[?:?]
    at java.security.AccessController.doPrivileged(Native Method) ~[?:?]
    at sun.security.ssl.SSLEngineImpl$DelegatedTask.run(SSLEngineImpl.java:1008) ~[?:?]
    at java.util.ArrayList.forEach(ArrayList.java:1541) ~[?:?]
    at jdk.internal.net.http.common.SSLFlowDelegate.lambda$executeTasks$3(SSLFlowDelegate.java:1073) ~[java.net.http:?]
    at jdk.internal.net.http.HttpClientImpl$DelegatingExecutor.execute(HttpClientImpl.java:153) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate.executeTasks(SSLFlowDelegate.java:1068) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate.doHandshake(SSLFlowDelegate.java:1034) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate$Reader.processData(SSLFlowDelegate.java:442) ~[java.net.http:?]
    at jdk.internal.net.http.common.SSLFlowDelegate$Reader$ReaderDownstreamPusher.run(SSLFlowDelegate.java:264) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$SynchronizedRestartableTask.run(SequentialScheduler.java:175) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:147) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$TryEndDeferredCompleter.complete(SequentialScheduler.java:315) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$CompleteRestartableTask.run(SequentialScheduler.java:149) ~[java.net.http:?]
    at jdk.internal.net.http.common.SequentialScheduler$SchedulableTask.run(SequentialScheduler.java:198) ~[java.net.http:?]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) ~[?:?]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) ~[?:?]
    ... 1 more
shred commented 3 weeks ago

Yes, it seems to be related to the java.net.http client.

I can reproduce this issue when I use a domain instead of localhost to connect to a Pebble container. I will have a look into that.

shred commented 3 weeks ago

You can use -Djdk.internal.httpclient.disableHostnameVerification to disable hostname verification. Since Pebble is only meant to be used in test environments, it should be an acceptable workaround. I could not reproduce the error anymore when I set that system property.

According to this stackoverflow question, it was a deliberate decision not to add a hostname verifier to the java.net.http client. If that's correct, the disableHostnameVerification system property is the only possible fix.

I will still try to find a proper solution this weekend, although I'm not too optimistic.

nsheridan commented 3 weeks ago

Yeah that's a workaround I'd found too, though hadn't tested yet. I can also use a different cert with the correct SAN. Thanks for looking into it - it's only a minor inconvenience but I wanted to flag it anyway.

shred commented 3 weeks ago

Yes, the correct approach is to generate an end-entity certificate on your Pebble server that matches your Pebble FQDN. This is how to create it: https://github.com/letsencrypt/pebble/tree/main/test/certs

disableHostnameVerification is the only alternative I see, with all the security implications of disabling hostname verification globally.

Thank you for the pointer! I will update the Pebble chapter of the acme4j documentation accordingly.

Closing this issue. Feel free to reopen if necessary.