Achieve external api calls using Spring WebClient and .p12 cert - spring

I have .p12 certificate inside my project placed into recources directory. All I want to do is to use this file to make external api calls. So I have read some info about how to achieve this:
private WebClient getWebClient() {
HttpClient httpClient = HttpClient.create();
httpClient.secure(spec -> {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
// Set up key manager factory to use key-store
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePass.toCharArray());
spec.sslContext(SslContextBuilder.forClient()
.keyManager(keyManagerFactory)
.build());
});
return WebClient
.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
After the external api call I get:
unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Thank you guys in advance for any solutions.

I propose to load p12 file via Resource loader API as an alternative approach.

Related

Get certificates from Azure KeyVault to Keystore in Kotlin/SpringBoot for outgoing requests

For some API requests I need in my backend a certificate. Currently the .p12 is versioned in the repository and loaded into the WebClient when its initialized like this:
private fun getWebClient(): WebClient {
val ks: KeyStore = KeyStore.getInstance("PKCS12")
ks.load(ClassPathResource("keystore.p12").inputStream, config.trustStorePassword.toCharArray())
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
kmf.init(ks, config.trustStorePassword.toCharArray())
val sslContext = SslContextBuilder
.forClient()
.keyManager(kmf)
.build()
val httpClient: HttpClient = HttpClient.create().secure { sslSpec -> sslSpec.sslContext(sslContext) }
return WebClient
.builder()
.baseUrl(config.BaseUrl)
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
}
I want to change it since the backend is deployed to Azure AppService. I already created a KeyVault and imported the certificate and granted access via a managed identity to the AppService.
I currently struggle to load the keystore in Spring Boot from the KeyVault. For reference I am trying to follow https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-key-vault-certificates.
It uses the azure.keyvault.uri property which is apparently deprecated, so I am using spring.cloud.azure.keyvault.certificate.endpoint.
Also documentation states:
KeyStore azureKeyVaultKeyStore = KeyStore.getInstance("AzureKeyVault");
KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter(
System.getProperty("azure.keyvault.uri"));
azureKeyVaultKeyStore.load(parameter);
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(azureKeyVaultKeyStore, null)
.build();
However, I am not able to resolve the class KeyVautLoadStoreParameter.
I am using spring boot 2.7.7 and
implementation("com.azure.spring:spring-cloud-azure-starter:4.5.0")
implementation("com.azure.spring:spring-cloud-azure-starter-keyvault:4.5.0")
Any help towards loading the certificates and configuring the WebClient would be greatly appreciated.
What you can do is create an app registration in azure AD and then grant access to this app registration through access policy in the azure keyvault.
Now collect the clientId , Tenant Id and client Secret from the app registration
Create the secret in certificates&secret tab and click on new registration.
Now create the application.yaml file in the following format
server:
ssl:
key-alias: TestCertificate
key-store-type: AzureKeyVault
trust-store-type: AzureKeyVault
port: 8443
azure:
keyvault:
uri: <KEYVAULT URL>
client-id: <CLIENT_ID FROM APP REGISTRATIOM>
client-secret: <CLIENT_SECRET FROM APP REGISTRATIOM>
enabled: true
tenant-id: <TENANT_ID FROM APP REGISTRATIOM>
Now you can import all the credentials from the azure key vault using the following program.
KeyStore azureKeyVaultKeyStore = KeyStore.getInstance("AzureKeyVault");
KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter(
System.getProperty("azure.keyvault.uri"),
System.getProperty("azure.keyvault.tenant-id"),
System.getProperty("azure.keyvault.client-id"),
System.getProperty("azure.keyvault.client-secret")
);
azureKeyVaultKeyStore.load(parameter);
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(azureKeyVaultKeyStore, null)
.build();
// Here I am cross check wether we have imported the certificate name " TestCertificate"
if(azureKeyVaultKeyStore.containsAlias("TestCertificate"))
{
System.out.println("The Certificate Exists ");
}
else {
System.out.println("Error Has Occured");
}
Output:

Unable to connect to SSL enabled Elastic Search from Google Dataflow

Unable to connect to SSL enabled Elastic Search using Google Cloud Dataflow. I have used google provide template from git I modified the source code to pass keystore, keystorepassword, truststore to the job along with the Elastic Search credentials
Elastic Search Class file
config = config.withKeystorePassword("XXXXXXXXXXXXXXXXXXXXXXXXXXX")
.withKeystorePath("PATH_TO_KEYSTORE")
Exception on running the job is as below
Caused by: java.lang.IllegalArgumentException: Cannot get
Elasticsearch version at
com.google.cloud.teleport.v2.elasticsearch.utils.ElasticsearchIO.getBackendVersion(ElasticsearchIO.java:1546)
at
com.google.cloud.teleport.v2.elasticsearch.utils.ElasticsearchIO$Write$WriteFn.setup(ElasticsearchIO.java:1336)
Caused by: org.elasticsearch.client.ResponseException: method [GET],
host [https://elastic.irds.opus.dev.gcp.db.com], URI [/], status line
[HTTP/1.1 401 Unauthorized]
{"error":{"root_cause":[{"type":"security_exception","reason":"missing
authentication credentials for REST request
[/]","header":{"WWW-Authenticate":"Basic realm="security"
charset="UTF-8""}}],"type":"security_exception","reason":"missing
authentication credentials for REST request
[/]","header":{"WWW-Authenticate":"Basic realm="security"
charset="UTF-8""}},"status":401} at
org.elasticsearch.client.RestClient.convertResponse(RestClient.java:302)
at
org.elasticsearch.client.RestClient.performRequest(RestClient.java:272)
at
org.elasticsearch.client.RestClient.performRequest(RestClient.java:246)
at
com.google.cloud.teleport.v2.elasticsearch.utils.ElasticsearchIO.getBackendVersion(ElasticsearchIO.java:1530)
at
com.google.cloud.teleport.v2.elasticsearch.utils.ElasticsearchIO$Write$WriteFn.setup(ElasticsearchIO.java:1336)
at
com.google.cloud.teleport.v2.elasticsearch.utils.ElasticsearchIO$Write$WriteFn$DoFnInvoker.invokeSetup(Unknown
Source)
We wrote a sample java program to ensure elastic search connectivity, which works fine as expected
SSLContextBuilder sslBuilder = SSLContexts.custom()
.loadKeyMaterial(keyStore,"*************".toCharArray())
.loadTrustMaterial(truststore,null);
final SSLContext sslContext = sslBuilder.build();
RestClientBuilder builder = RestClient.builder(
new HttpHost("https://X.X.X.X", 443, "https"))
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
public HttpAsyncClientBuilder customizeHttpClient(
final HttpAsyncClientBuilder httpAsyncClientBuilder) {
httpAsyncClientBuilder.disableAuthCaching();
return httpAsyncClientBuilder.setSSLContext(sslContext);
}
});
RestHighLevelClient client = new RestHighLevelClient(builder);
GetRequest getRequest = new GetRequest("test_index1/_search", "1");
GetResponse response = client.get(getRequest, RequestOptions.DEFAULT);
System.out.println("\nSearch results:");
System.out.println(response.getSourceAsString());
Could you please provide some inputs on what changes do i need to make on dataflow template to connect to SSL enabled Elastic search

Invalid Certification with Amazon S3 Presigned URL

I am trying to download an object from an S3 bucket using a presigned url via the following configuration & code:
public void getDocumentFromPresignedUrl(final String presignedUrl, final String id) {
PresignedUrlDownload transfer = null;
try {
File file = File.createTempFile(id, ".pdf");
//errors out on this line
transfer = getTransferMgr().download(new PresignedUrlDownloadRequest(new URL(presignedUrl)), file);
transfer.waitForCompletion();
}
}
Which is configured via the following:
private ClientConfiguration getClientConfiguration() {
ClientConfiguration clientConfig = new ClientConfiguration();
clientConfig.setProtocol(Protocol.HTTPS);
return clientConfig;
}
public TransferManager getTransferMgr() {
return TransferManagerBuilder.standard().withS3Client(getS3Client()).build();
}
public AmazonS3 getS3Client() {
return AmazonS3ClientBuilder.standard().withRegion(region)
.withClientConfiguration(getClientConfiguration()).build();
}
However, the following error is thrown each time:
com.amazonaws.SdkClientException:
Unable to execute HTTP request: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
WHAT I HAVE TRIED:
I tried to take the AWS cert from the presigned url location in-browser, discussed here
I tried to use the traditional RestTemplate provided by Spring, with no luck
I AM able to retrieve the object from S3 both in Postman and my browser, but not via my Spring app
How does one circumvent this sdkClientException and GET their S3 object?
The solution to this problem resides in cacerts. If you are running on a proxy (such as Zscaler), the certification will need to be added to the /cacerts file.
In the case of this question, I was adding the Zscaler cert to the WRONG JRE. Because I was using SpringToolSuite, I needed to add the cert to Spring's JRE, which in my case was:
keytool -import -alias Z_Root -keystore "C:\Program Files\sts-4.8.1.RELEASE\plugins\org.eclipse.justj.openjdk.hotspot.jre.full.win32.x86_64_1.v20201010-1246\jre\lib\security\cacerts" -storepass changeit -file "C:\Users\myUser\Downloads\MyZscalerCert.crt"
and NOT the typical %JAVA_HOME%/jre path.

Accessing OAuth2 protected Resource with Spring WebClient where the token provider is also ssl protected

trying to use webclient to access an OAuth2 protected token and resource server. The requirement is to add certificates to both requests. Here is more or less how I try to add certs to my request:
HttpClient resourceServerHttpClient = HttpClient.create()
.tcpConfiguration(client -> client.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000))
.secure(sslContextSpec -> {
sslContextSpec.sslContext(oAuth2ClientSSLPropertiesConfigurer.getConstructedSslContexts().get("cpd"));
});
ClientHttpConnector customHttpConnector = new ReactorClientHttpConnector(resourceServerHttpClient);
return WebClient.builder()
.baseUrl(lisClientBaseUrl)
.exchangeStrategies(exchangeStrategies)
.clientConnector(customHttpConnector)
.filter(oauth)
.filters(exchangeFilterFunctions -> {
exchangeFilterFunctions.add(logRequest());
exchangeFilterFunctions.add(logResponse());
})
.build();
Question 1 : The snippet above passes certificates to the resource server request only, as far as I can understand. Found https://neuw.medium.com/spring-boot-oauth2-mutual-tls-client-client-credentials-grant-3cdb7a2a44ea where my customization fits Scenario-3. But then the only way is to switch fully reactive context (not servlet) in my api, to access DefaultReactiveOAuth2AuthorizedClientManager, authorizedClientProvider etc. Is there a simpler way to achieve this?
Question 2 : Is there a way to see more details regarding the request to the token provider - to make sure that certificates are sent.

How to use Spring WebSocketClient with SSL?

I connect with my websocket client to non-SSL endpoint without any problem. But I cannot find any way how to connect to wss (SSL) endpoint. Where can I define the SSL factory etc. No object seem to have related set method.
WebSocketClient transport = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(transport);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
String url = cfg.getWebsocketEndpoint();
StompSessionHandler handler = new MySessionHandler();
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
stompClient.connect(url, handler);
I am using wss:// url and on the other side I have a server with self-signed certificate. However, this code does not throw any exception while connecting, but the session is not established.
EDIT: After enabling tracing for web.* I got a standard error, with
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
It occurs when connecting to server with self-signed certificate. However, for RestTemplate I already updated SSLContext with this code and REST calls are fine now, but I do not know why, StandardWebSocketClient is IGNORING the SSLContext. Why?
String keystoreType = "JKS";
InputStream keystoreLocation = new FileInputStream("src/main/resources/aaa.jks");
char [] keystorePassword = "zzz".toCharArray();
char [] keyPassword = "zzz".toCharArray();
KeyStore keystore = KeyStore.getInstance(keystoreType);
keystore.load(keystoreLocation, keystorePassword);
KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmfactory.init(keystore, keyPassword);
InputStream truststoreLocation = new FileInputStream("src/main/resources/aaa.jks");
char [] truststorePassword = "zzz".toCharArray();
String truststoreType = "JKS";
KeyStore truststore = KeyStore.getInstance(truststoreType);
truststore.load(truststoreLocation, truststorePassword);
TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(truststore);
KeyManager[] keymanagers = kmfactory.getKeyManagers();
TrustManager[] trustmanagers = tmfactory.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keymanagers, trustmanagers, new SecureRandom());
SSLContext.setDefault(sslContext);
UPDATE: Unfortunately, I did not managed to do this with custom truststore. I installed the certificate with InstallCert.java.
I think that each websocket container implementation provides ways to do this.
You have to set this configuration using the StandardWebSocketClient .setUserProperties. All those properties are internally set in the ClientEndpointConfig used by the client.
Here's an example with Tomcat as a provider:
StandardWebSocketClient wsClient = //...;
SSLContext sslContext = //...;
wsClient.setUserProperties(WsWebSocketContainer.SSL_CONTEXT_PROPERTY, sslContext);
In any case you should refer to your provider reference documentation to know which configuration keys you should use.
I've faced a similar issue, where I need to use a different ssl context instead of the default one and I could not use the Tomcat provider version solution. I've met a lot of resources and examples about this solution which does not fit my case, and for sure I landed here too on this question.
When you need to use a specific/custom SSLContext to set and you don't need the 'tomcat version', you can go for the Jetty version because it allows you to set which trust store and/or key store you want to use. In this case my Spring application was on the 'client' side and not the server one, but Jetty SslContextFactory provides the same functionalities for the 'Server' case.
The short example code below is for the client side, but it shares methods and signatures with the server side (check the jetty documentation about)
final SslContextFactory.Client factory = new SslContextFactory.Client();
factory.setSslContext(sslContext); // a different loaded java SslContext instead of the default one
//Create the web socket client with jetty client factory
final JettyWebSocketClient client =
new JettyWebSocketClient(new WebSocketClient(new HttpClient(factory)));
client.start();
final WebSocketStompClient stomp = new WebSocketStompClient(client);
There're also method for setting the trust store or key store instead a fully loaded SslContext.

Resources