I am calling an external API via RestTemplate that requires SSL Certificate. Already the external API provider has provided me with the certificate file (company.crt) and its key file (company.key). While adding the certificate, key and password in Postman, I am able to successfully call the API. But when I am calling using RestTemplate inside a SpringBoot project, even after adding SSLContext, I am receiving an error.
The steps I have followed:
Created a company.p12 store file from company.crt and company.key file using openssl:
openssl pkcs12 -export -name servercert -in company.crt -inkey company.key -out company.p12
Converted company.p12 to company.jks store file using Keytool:
keytool -importkeystore -destkeystore company.jks -srckeystore company.p12 -srcstoretype pkcs12 -alias companycert
Have created the config inside application.properties file as, after placing company.jks inside resource folder of SpringBoot project:
http.client.ssl.trust-store=classpath:company.jks
http.client.ssl.trust-store-password=Password
Then, I have created a configuration for RestTemplate as:
#Value("${http.client.ssl.trust-store}")
private Resource keyStore;
#Value("${http.client.ssl.trust-store-password}")
private String keyStorePassword;
#Bean
RestTemplate restTemplate() throws Exception {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(
keyStore.getURL(),
keyStorePassword.toCharArray()
).build();
SSLConnectionSocketFactory socketFactory =
new SSLConnectionSocketFactory(sslContext);
HttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(socketFactory).build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(factory);
}
Calling an external APIs as:
#Autowired
RestTemplate restTemplate;
#GetMapping("/api")
public Object callAPI() {
final String ENDPOINT = "https://some-api.domain.com:8533/api/key/";
Object response = restTemplate.exchange(ENDPOINT, HttpMethod.GET, request, Object.class);
return response;
}
The error after calling an API via RestTemplate:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Got it resolved:
application.properties configuration: Add Key store properties
http.client.ssl.trust-store=classpath:company.jks
http.client.ssl.trust-store-password=Password
http.client.ssl.key-store=classpath:company.p12
http.client.ssl.key-store-password=Password
Then, inside RestTemplate configuration:
#Bean
RestTemplate restTemplate() throws Exception {
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(
trustStore.getURL(),
trustStorePassword.toCharArray(), acceptingTrustStrategy
)
.loadKeyMaterial(
keyStore.getURL(),
keyStorePassword.toCharArray(),
keyStorePassword.toCharArray())
.build();
SSLConnectionSocketFactory socketFactory =
new SSLConnectionSocketFactory(sslContext);
HttpClient httpClient = HttpClients.custom()
.setSSLSocketFactory(socketFactory).build();
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(factory);
}
Related
my client side i have attached the certificate in the restTemplate code below . using this rest template i am calling other API(server side) . how to get the certificate in that server side
#Bean(name="custRest")
#Primary
public RestTemplate restTemplate(RestTemplateBuilder builder) throws Exception {
char[] password = "changeit".toCharArray();
TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
SSLContext sslContext = SSLContextBuilder
.create().loadKeyMaterial(new File("C:\\java\\java-certificate.der"),null,null)
.loadTrustMaterial(ResourceUtils.getFile("C:\\java\\java-certificate.der"), null,
acceptingTrustStrategy)
.build();
HttpClient client = HttpClients.custom().setSSLContext(sslContext).build();
return builder.requestFactory(new HttpComponentsClientHttpRequestFactory(client))
.build();
}
You can find all step for configuring HTTPS for Spring Boot in article:
https://www.baeldung.com/spring-boot-https-self-signed-certificate
UPDATE:
Here is the link to the official API documentation.
And here is a video with the requests I made from NodeJS and here is the java client app that I implemented.
I have a request in NodeJS and now I want to implement the same request in Java for my Android app.
The NodeJS request looks like this:
var options = {
method: 'GET',
url: 'https://webapi.developers.erstegroup.com/api/bcr/sandbox/v1/aisp/v1/accounts',
cert: "-----BEGIN CERTIFICATE-----....-----END CERTIFICATE-----",
key: "-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----",
headers:
{
'x-request-id': '30fb2676-8c2e-11e9-b683-526af7764f64',
'web-api-key': '#########',
'Accept': 'application/json'
}
};
request(options, function (error, response, body) {
console.log(body);
});
My problem is that I don't know how to include that certificate and private key to make the request.
I found this answer regarding how to read the certificate and the key in Java, but I don't know how to configure the SSLContext to use the certificate and also the key.
Currently, I tried the next solution, but don't work. The first problem I have is that I get an error when I parse the key:
The error message: org.bouncycastle.openssl.PEMException: malformed sequence in RSA private key
The method used to read the key:
private static PrivateKey readPrivateKey(String filename) throws Exception {
PEMParser pemParser = new PEMParser(new BufferedReader(new InputStreamReader(Main.class.getClassLoader().getResourceAsStream(filename))));
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PEMKeyPair pemKeyPair = (PEMKeyPair) pemParser.readObject();
KeyPair kp = converter.getKeyPair(pemKeyPair);
return kp.getPrivate();
}
The private key:
-----BEGIN RSA PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDJZunh6Nj5aBtb
hJJ9eaSDjDiiERBOZfVrmEwz9Ea5ldLAf8NUy+t71etzGeHtCKyTuSlhj1Clhla+
iG/r1uz25H3T6wUQbmw1+pFsaSovkamQaKy+2GJSPCx66li6z2JBv0I66DtoGl6Q
Xcy5JO2sBjZaO4m11bcFFVkvo9lh2QCC2x68w8bHeBuPUMnU6rupVQfgPPWMH+Wq
qaPgxoQr5KmQ03ItY2/TBqoX6xTTbL6+B8OMbX41Lxah+g+5XPOlDoC64HiBO2T+
FL62W+51ehUCORuuUt6/AYzXcCHSu1FXsk25KeObe9Na2AfobGNL83I68Bl0K2Wt
jbEtHs1FAgMBAAECggEAb7K3Bga4x2IYwiH9iL99IUQUaLXkAEcF3N2DbdENpIHW
d9KkB5RtDqouwhBZv7du1yL7M1Njm9mspFFRGVCC7c79hhmzHlDPjQRhwOl2bxlv
HFsha1rg9NDQrn7oJPs9eE9VsQv5Xpw5VAHht9EmS6DKZjLdBk74CUa0xvotZtkS
UrvRLOWhedsS0Ckf3vDfqU5NZBEecEj1vLGnD1ET6znRhag2VqUYS83fKUR5UpLq
X4PqMfYucF40ddd+L/iMeAnGmKukYEfew1R3ez5rKSS+cyJkoAKtL1WR0nFSDskw
zFQaf6PNIUIL7CDnOdUlyiRYx6a5y6NzJcQyI0bn5QKBgQDhUPck4ekVT8z29Llf
kkXWoF15l7KNiiq2DZfWlH2pax4G3NBUimb62fHcSQVovD0aLCJOF8N0n6vv5YbF
MIURRWXRTNxO2S16xUpUMD2Ospv50UpNIMAJlgnkTt/DTsd6MYQx2j+qyf4wPvWO
QSOQNpdebd/59LWbAye5WROuJwKBgQDk1D4sEWti9dR0LTJS0B+FHLPPJhpNg+cD
zqFEXvSICQjAhyJ3Fir/u3HhX4966+dhODaDphAOQcG+4iyXUQVEh+qQJ5p+MJQU
ue0yZgQvPIo5a+gnFyzEmCOtaENBqJqK1tbCklFZtbJswVEtlqjq+qMAzjNOzf2F
6krA9VC4swKBgQCIrJ5eJxNGNDP2kZho2se2W2yYN2a96NPjvvcd2NEpFasPKp7M
yW+SNuY5Y6n+UEEYQTFGAbA0bC7VxHst3jK5uUj73w28Xozx7f8adnDAwKNQtJ3H
j1gt+G9jqFyfkof6HVM9ElCQfxrLlUVK10SFVDgZtbipXMFUmGNeUSRY/QKBgQCW
a/Lmsxi9d84OBLvk9k0SCrkkfe6icAfHV+ho8maamh23ud1tHRRtAYIt3cyKyFJU
dUhYqCw7wvwih7k6SxdEYnhOBMqpEzP0n/gNvkQX7RsL/iQgtjpGjaA+WKCFo9jb
VbjdNKPnbep5VWcQqc4mkVXfrKzLq9txUX+McnZ6wwKBgAy2RVQ/ja3mvVUiZ3ZN
U4Y+nObKV6Z9JoMC4VNbbwufVPSoj/j20jj8uB29WupQ/BG2W0jHS3GfjrwH9IiN
7Pm+Nco8E/Yywh7YYNFJOxNesc7kB7RJuhfTabif9Ea+LqF6CQQ10rHzzT3WlY5p
rAJmu7k+JlKn5Aad+KQF4RXJ
-----END RSA PRIVATE KEY-----
The rest of the code:
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
final X509Certificate cert = readCertificate("public-key-bcr.cer");
keyStore.setCertificateEntry("key-bcr", cert);
keyStore.setKeyEntry("key-bcr", readPrivateKey("private-key-bcr.key"), "".toCharArray(), new Certificate[]{cert});
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
final SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(null, tmf.getTrustManagers(), null);
final OkHttpClient.Builder builder = new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory())
.hostnameVerifier(new HostnameVerifier() {
#Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
OkHttpClient client = builder.build();
Also, I want to know if there is an easier alternative.
TLDR: your keyfile is labelled wrong.
Your privatekey file has PEM labels claiming it is RSA PRIVATE KEY which according to the de-facto standard should contain data which is the encoded form of a PKCS1-format privatekey. However, the data in your file is actually a PKCS8-format PrivateKeyInfo, which per rfc7468 should have labels PRIVATE KEY (NO RSA).
If you correct your file's labels to PRIVATE KEY with no RSA, BouncyCastle can read it, but you need to change the type and method used:
PEMParser pemParser = new PEMParser(/* appropriate Reader */);
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
// you don't actually need to set the provider, the default provider(s) work fine.
PrivateKeyInfo privkey = (PrivateKeyInfo) pemParser.readObject();
return /*PrivateKey*/ converter.getPrivateKey(privkey);
However, you don't need BouncyCastle; this (unencrypted) format can be read by Java crypto directly:
String pem = /* read all chars from file/resource/whatever, or read all bytes and convert to String */;
byte[] der = Base64.getDecoder().decode( pem.replaceAll("-----(BEGIN|END) PRIVATE KEY-----\r?\n", "") );
KeyFactory fact = KeyFactory.getInstance("RSA");
return /*PrivateKey*/ fact.generatePrivate(new PKCS8EncodedKeySpec(der));
Finally, your security scheme doesn't make sense. You aren't actually using this key for anything; you seem to be using the cert, only, as a CA cert (trust anchor). If this cert is indeed a CA cert, by including the CA's privatekey in your app you have allowed everyone who has a copy of the app to replace or impersonate your server(s?) and steal, modify, or destroy all your data -- and publishing it on Stack extends this to everyone in the world. Your nodejs code, in contrast, configures this key&cert to be used as a client key&cert and not as a CA or anchor at all -- although the server you specify in your URL doesn't appear to request a client key&cert at all, and it uses a cert under a normal root (Digicert) that doesn't require any modification to the Java defaults, and has a correct serve rname which also doesn't require disabling hostname verification.
The problem was that you are not able to use .cert and .key files in Java because the KeyStore doesn't know to work with these files.
To fix it you need to convert your files to .p12 (PKCS#12) file. To do this I used KeyStore Explorer.
After I converted the file I was able to make the call.
The solution code is here:
private static SSLSocketFactory getFactory(String fileName, String password) {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(Main.class.getClassLoader().getResourceAsStream(fileName), password.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, password.toCharArray());
SSLContext context = SSLContext.getInstance("SSL");
context.init(
keyManagerFactory.getKeyManagers(),
null,
new SecureRandom()
);
return context.getSocketFactory();
}
public static void main(String[] args) throws Exception {
final OkHttpClient.Builder builder = new OkHttpClient.Builder()
.sslSocketFactory(getFactory("converted_file.p12", "1234"))
.hostnameVerifier((hostname, session) -> true);
OkHttpClient client = builder.build();
//...
}
I'm building a Spring WebClient which internally calls to REST API's which are hosted in different server. To do that I need to send public key (.cert) and private key (.key) to every request for the handshake.
I'm not sure how to do that with Spring WebClient.
I tried setting up WebClient, but struck at adding this peace of code
WebClient Builder
this.webCLient = WebClient.builder()
.baseUrl("https://some-rest-api.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString())
.build();
Actual Call
this.webClient.get()
.uri("/getData")
.exchange()
.flatMap(clientResponse -> {
System.out.println(clientResponse);
return clientResponse.bodyToMono(MyClass.class);
});
Since there were no certificates added to the request, I'm getting the handshake error on the log
javax.net.ssl.SSLException: Received fatal alert: handshake_failure
How to add those certificates to the WebClient requests, so I don't get this error ? I have the certificates, but not sure how to add it.
It took me some time to find the missing piece in Thomas' answer.
Here it is:
public static SslContext getTwoWaySslContext() {
try(FileInputStream keyStoreFileInputStream = new FileInputStream(ResourceUtils.getFile(clientSslKeyStoreClassPath));
FileInputStream trustStoreFileInputStream = new FileInputStream(ResourceUtils.getFile(clientSslTrustStoreClassPath));
) {
KeyStore keyStore = KeyStore.getInstance("jks");
keyStore.load(keyStoreFileInputStream, clientSslKeyStorePassword.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, clientSslKeyStorePassword.toCharArray());
KeyStore trustStore = KeyStore.getInstance("jks");
trustStore.load(trustStoreFileInputStream, clientSslTrustStorePassword.toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(trustStore);
return SslContextBuilder.forClient()
.keyManager(keyManagerFactory)
.trustManager(trustManagerFactory)
.build();
} catch (Exception e) {
log.error("An error has occurred: ", e);
}
return null;
}
HttpClient httpClient = HttpClient.create().secure(sslSpec -> sslSpec.sslContext(SslUtil.getTwoWaySslContext()));
ClientHttpConnector clientHttpConnector = new ReactorClientHttpConnector(httpClient);
WebClient webClient = webClientBuilder
.clientConnector(clientHttpConnector)
.baseUrl(baseUrl)
.build();
Enjoy!
taken from the documentation Spring Webclient - Reactor Netty
To access the ssl configurations you need to supply a custom netty HttpClient with a custom sslContext.
SslContext sslContext = SslContextBuilder
.forClient()
// build your ssl context here
.build();
HttpClient httpClient = HttpClient.create().secure(sslSpec -> sslSpec.sslContext(sslContext));
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
I'm trying to execute requests to a server which provided me with a .p12 file in order to make secure connection with rest services, I'm doing the following in order to set the HttpClient with the key:
SSLContext sslContext =SSLContextBuilder
.create().loadKeyMaterial(ResourceUtils.getFile("classpath:keystore/file.p12"), "secret".toCharArray(), "secret".toCharArray())
.build();
return HttpClientBuilder
.create()
.setConnectionManager(connManager())
.setSSLContext(sslContext)
.setDefaultRequestConfig(requestConfig())
.build();
When I execute the request with OAuth2RestOperations I got:
401 , Non existing certificate or invalid
I recently had a similar requirement. Here is the code I used:
KeyStore clientStore = KeyStore.getInstance("PKCS12");
try {
clientStore.load(ResourceUtils.getFile("classpath:keystore/file.p12"), "secret".toCharArray());
} catch (IOException e) {
//handle exception
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientStore, "secret".toCharArray());
KeyManager[] kms = kmf.getKeyManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kms, null, new SecureRandom());
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);
HttpClientBuilder builder = HttpClientBuilder.create();
return builder.setSSLSocketFactory(socketFactory).build();
I think this is actually a duplicate question.
Please see this answer for this question Java HTTPS client certificate authentication.
In all examples you need to call loadKeyMaterial method with KeyStore
public SSLContextBuilder loadKeyMaterial(KeyStore keystore,
Load the keyStore using file path, for example:
keyStore = KeyStore.getInstance("PKCS12");
FileInputStream inputStream = new FileInputStream(new File(certPath));
keyStore.load(inputStream, certPassword.toCharArray());
i have key and cert (combined) into one cert.pem file ,
and i getting ,
"exception": "javax.net.ssl.SSLHandshakeException",
"message": "Received fatal alert: bad_certificate",
pem file is right, but i think problem is how i generating jks keystore file.
.pem cert format
BEGIN CERTIFICATE
...
END CERTIFICATE
BEGIN CERTIFICATE
...
END CERTIFICATE
BEGIN RSA PRIVATE KEY
...
END RSA PRIVATE KEY###`
combine it with keytool comand comand is
keytool -import -trustcacerts -alias yourdomain -file combined.pem -keystore yourkeystore.jks
java code is
public class HttpsTrustManager implements X509TrustManager {
#Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
// TODO Auto-generated method stub
}
#Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
// TODO Auto-generated method stub
}
#Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
request is
FileInputStream instream = new FileInputStream(
new File(this.resourcePath()+"/path_to.jks")
);
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(instream, "password".toCharArray());
SSLContext sslContext = SSLContexts.custom()
.loadKeyMaterial(keyStore, "password".toCharArray()) // use null as second param if you don't have a separate key password
.build();
sslContext.init(null,new X509TrustManager[]{new HttpsTrustManager()}, new SecureRandom());
HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
HttpResponse response = httpClient.execute(
new HttpPost("https://url")
);
HttpEntity entity = response.getEntity();
System.out.println("----------------------------------------");
System.out.println(response.getStatusLine());
EntityUtils.consume(entity);
When you use Apache SSLContexts.custom().loadKeyMaterial().build() it initializes the built context with the specified keystore and the default trustmanager. You then call sslContext.init() to re-initialize it with no keymanager and the specified trustmanager; this ignores and discards the prior initialization. As a result your context has no keymanager, and cannot do client auth.
You need to be consistent. Either use Apache and give the (same) builder both loadKeyMaterial and loadTrustMaterial corresponding to what you want -- in particular httpclient 4.5.4 adds org.apache.http.conn.ssl.TrustAllStrategy which implements "cheerfully let all thieves and crooks see and change my supposedly secure data". Alternatively, use JSSE to directly create an SSLContext with .getInstance() and .init() it (once!) with your zero-security trustmanager and a keymanager created from your keystore (and an explicit SecureRandom if you like but if you omit that it defaults).
However, this may not work because the keytool command you show is correct only if yourdomain was a pre-existing PrivateKeyEntry matching the cert chain you imported to it. Use keytool -list -alias yourdomain to make sure it's a PrivateKeyEntry and NOT a TrustedCertEntry. If not, and if you need to use the privatekey from the PEM file (rather than one already in a keystore) you need to first convert the key and cert chain to PKCS12 with OpenSSL, and then depending on your Java maybe convert the PKCS12 to JKS with keytool. There are dozens of Qs (and As) on several Stacks for this.