I'm trying to use configured host-specific outbound SSL configuration in OpenLiberty to make an https connection to a remote system (Apple Pay, in this case, but I don't think relevant).
I know the keystore and password and protocols are correct, because when I do it entirely programmatically, like shown in https://stackoverflow.com/a/63080443/796761, the connection and request fully work.
However, when I remove all the keystore/SSL setup and try to fall back on what I've configured in server.xml
, I get the same error as I get when I'm not setting up SSL at all. (Which, in this case is java.io.IOException: HTTP/1.1 header parser received no bytes
.)
Relevant server.xml
elements:
<featureManager>
<feature>webProfile-8.0</feature> <!-- adds ssl-1.0 -->
...
</featureManager>
...
<keyStore id="applePayKeyStore" location="c:/keys/MerchID.AZMVDNOW.p12" password="redacted"/>
<ssl id="applePaySSL" keyStoreRef="applePayKeyStore" clientAuthentication="true" sslProtocol="TLSv1.2">
<outboundConnection host="apple-pay-gateway-cert.apple.com"/>
</ssl>
Java code:
HttpRequest request = HttpRequest.newBuilder().uri(validationEndpoint)
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(jsonRequest))
.build();
HttpClient client = HttpClient.newBuilder().build();
return client.send(request, BodyHandlers.ofString()).body();
Where validationEndpoint
is set to https://apple-pay-gateway-cert.apple.com/paymentservices/paymentSession
Again, if I instead replace the above client
with the following, the request succeeds:
char[] passwordChars = keystorePassword.toCharArray();
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("/keys/MerchID.AZMVDNOW.p12"), passwordChars);
KeyManagerFactory keyMgrFactory = KeyManagerFactory.getInstance("SunX509");
keyMgrFactory.init(keyStore, passwordChars);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(keyMgrFactory.getKeyManagers(), null, null);
SSLParameters sslParam = new SSLParameters();
sslParam.setNeedClientAuth(true);
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext)
.sslParameters(sslParam).build();
OpenLiberty 24.0.0.6, JDK 17
So, the question is how can I get Liberty to use this specific keystore when making an https connection to the specified server?
I haven't so far found an example of making such a connection with Liberty automatically using the configured SSL configuration, so I hope I'm just missing some simple glue.
javax.net.debug=ssl
doesn't tell me anything helpful:
[err] javax.net.ssl|DEBUG|69|HttpClient-1-Worker-0|2024-07-12 20:30:31.041 UTC|SSLCipher.java:1870|KeyLimit read side: algorithm = AES/GCM/NOPADDING:KEYUPDATE
countdown value = 137438953472
[err] javax.net.ssl|DEBUG|69|HttpClient-1-Worker-0|2024-07-12 20:30:31.041 UTC|SSLCipher.java:2024|KeyLimit write side: algorithm = AES/GCM/NOPADDING:KEYUPDATE
countdown value = 137438953472
[err] javax.net.ssl|DEBUG|69|HttpClient-1-Worker-0|2024-07-12 20:30:31.051 UTC|SSLCipher.java:1870|KeyLimit read side: algorithm = AES/GCM/NOPADDING:KEYUPDATE
countdown value = 137438953472
[err] javax.net.ssl|ALL|69|HttpClient-1-Worker-0|2024-07-12 20:30:31.053 UTC|X509Authentication.java:223|No X.509 cert selected for [RSA, EC]
[err] javax.net.ssl|DEBUG|69|HttpClient-1-Worker-0|2024-07-12 20:30:31.053 UTC|SSLCipher.java:2024|KeyLimit write side: algorithm = AES/GCM/NOPADDING:KEYUPDATE
countdown value = 137438953472
[err] javax.net.ssl|DEBUG|91|Finalizer thread|2024-07-12 20:30:31.127 UTC|SSLSocketImpl.java:577|duplex close of SSLSocket
[err] javax.net.ssl|DEBUG|91|Finalizer thread|2024-07-12 20:30:31.129 UTC|SSLSocketImpl.java:1785|close the SSL connection (passive)
With Liberty SSL tracing, it looks like the relevant objects loaded up successfully, including this entry showing that Liberty was able to see the certificate in the store:
[7/12/24, 20:49:04:920 UTC] 00000031 id=00000000 com.ibm.ws.ssl.config.WSKeyStore 3 alias: apple pay merchant identity certificate
And mapped the host name to the configuration:
[7/12/24, 20:49:05:241 UTC] 00000031 id=00000000 com.ibm.ws.channel.ssl.internal.SSLChannelProvider 3 setSslConfig id=applePaySSL {com.ibm.ws.ssl.internal.RepertoireConfigService, com.ibm.wsspi.ssl.SSLConfiguration}={verifyHostname=false, service.scope=bundle, component.name=com.ibm.ws.ssl.internal.RepertoireConfigService, TrustStore.target=(service.pid=com.ibm.ws.ssl.keystore_0), trustDefaultCerts=false, KeyStore.target=(service.pid=com.ibm.ws.ssl.keystore_0), sslProtocol=TLSv1.2, config.source=file, outboundConnection.0.host=apple-pay-gateway-cert.apple.com, id=applePaySSL, service.pid=com.ibm.ws.ssl.repertoire_0, clientAuthenticationSupported=false, service.id=621, keyStoreRef=com.ibm.ws.ssl.keystore_0, enforceCipherOrder=false, service.bundleid=132, outboundConnection.0.config.referenceType=com.ibm.ws.ssl.repertoire.config.outboundConnection, osgi.ds.satisfying.condition.target=(osgi.condition.id=true), config.overrides=true, clientAuthentication=true, component.id=487, effectiveTrustStore=com.ibm.ws.ssl.keystore_0, config.id=com.ibm.ws.ssl.repertoire[applePaySSL], securityLevel=HIGH, service.factoryPid=com.ibm.ws.ssl.repertoire, service.vendor=IBM, config.displayId=ssl[applePaySSL]}
But I think the outbound connection used the defaultSSLConfig
instead of the applePaySSL
one:
[7/12/24, 20:50:43:868 UTC] 00000046 id=00000000 com.ibm.ws.ssl.SSLPropertyUtils > lookupProperties alias=defaultSSLConfig Entry [7/12/24, 20:50:43:868 UTC] 00000046 id=00000000 com.ibm.ws.ssl.SSLPropertyUtils > getProperties sslAliasName=defaultSSLConfig currentConnectionInfo={com.ibm.ssl.direction=outbound}
I can't paste the entire huge trace here, but can provide specifics if needed.
On OutboundSSLSelections
:
[7/15/24, 20:29:11:882 UTC] 00000031 id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 loadOutboundConnectionInfo
Adding apple-pay-gateway-cert.apple.com,* to the host list
From a later test just now, I do see these when looking up other host names:
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 SSLConfig dynamic selection info: apple-pay-gateway-cert.apple.com,*
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 This entry has 2 attributes.
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 Host: apple-pay-gateway-cert.apple.com, Port: *
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 Host does not match.
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 No match found list
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections < lookForMatchInList Exit
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 Cache miss tree set size is 2 entries.
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections 3 No match found in host or host and port list.
[7/17/24, 12:40:30:595 UTC] 0000003b id=00000000 com.ibm.ws.ssl.config.OutboundSSLSelections < getPropertiesFromDynamicSelectionInfo Exit
But no entries at all for com.ibm.ssl.remoteHost
with the apple host.
Based on previous answers and comments, I confirm that this works, but with a dependence on WebSphere-specific classes:
SSLContext sslContext =
JSSEHelper.getInstance().getSSLContext("applePaySSL", null, null);
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext).build();
(Where I also had to add trustDefaultCerts="true"
to the <ssl id="applePaySSL" >
element shown above in server.xml.)
To remove that dependence from the Java code, I looked for a way to inject the generic SSLContext
instance instead. Here's what worked for our Spring DI XML configuration:
<bean id="libertyJSSEHelper" class="com.ibm.websphere.ssl.JSSEHelper" factory-method="getInstance"/>
<bean id="appleSSLContext" factory-bean="libertyJSSEHelper" factory-method="getSSLContext">
<constructor-arg index="0" value="applePaySSL"/>
<constructor-arg index="1"><null/></constructor-arg>
<constructor-arg index="2"><null/></constructor-arg>
</bean>
Reducing the above code to
HttpClient client = HttpClient.newBuilder()
.sslContext(this.getSslContext()).build();