I want to call a REST service with my spring application. To access that service i have a client certificate (self signed and in X.509 format) for authorization. What is the proper way to authenticate against the rest service?
I have two certificate file and one private key that I want send to service in each request
private final String CLIENT_CERT = "C:\\Cert\\cert.cert";
private final String CLIENT_KEY = "C:\\Cert\\client.cert";
private final String LYNX_ROOT_CERT = "C:\\Cert\\root.crt";
This is my request:
private ResponseEntity<String> restTemplateGetForObject(UriComponentsBuilder builder,
Map<String, String> uriParams) {
HttpEntity<?> entity = getEntityWithHeaders(HttpMethod.POST);
ResponseEntity<String> resp = restTemplate.exchange(builder.buildAndExpand(uriParams).toUri(), HttpMethod.POST,
entity, String.class);
return resp;
}
public ResponseEntity<String> updateOrCreateAlarm() {
String BaseUrl = configuration.getUrl();
String port = configuration.getPort();
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(BaseUrl + ":" + port + "/api/v1/alarm");
Map<String, String> uriParams = new HashMap<String, String>();
uriParams.put("alarm_group", "LYNXKEYPRO");
uriParams.put("alarm_channel", "001");
uriParams.put("alarm_state", "ALARM");
uriParams.put("ip_address", "10.6.1.42");
uriParams.put("computer_name", "WS-B2-Lab1");
uriParams.put("version", "2");
uriParams.put("additional_text", "Custom Text to display with alarm");
return restTemplateGetForObject(builder, uriParams);
}
1) I would create a truststore and add this certificate as an entry there.
2) When start your rest client, make sure the truststore is included in the jvm process.
Here are the details:
Self-signed certificate (For Windows). Note, in your case you probably already have the certificate. Just make sure it is added into the truststore. Also in the dev environment, keystore and truststore can be the same. You can google how to do it. I used keystore Explorer.
cd certificate directory
"C:\Program Files\Java\jdk1.8.0_162\bin\keytool.exe" -genkey -alias signFiles -keystore badsslkeystore
badsslkeystore – keystore name
assword – changeit.
-alias – keystore entry, in our case it is signFiles.
http://java-buddy.blogspot.cz/2016/07/java-example-of-ssl-server-and-client.html
For client:
$ java -jar -Djavax.net.ssl.trustStore=keystore -Djavax.net.ssl.trustStorePassword=password "...JavaSSLClient.jar"
Or set them as System properties.
public static void setClientSslConfig() {
System.setProperty("javax.net.ssl.trustStore", TRUSTSTORE);//TRUSTSTORE - truststore location in the file system.
System.setProperty("javax.net.ssl.trustStorePassword", KEYSTORE_PASSWORD);
}
Related
I'm trying to implement the solution presented in the following AWS article:
How to Eliminate the Need for Hardcoded AWS Credentials in Devices
by Using the AWS IoT Credentials Provider
So I did next steps:
Create local keystore:
keystore winpty openssl pkcs12 -export -in eeb81a0eb6-certificate.pem.crt -inkey eeb81a0eb6-private.pem.key -name myname -out my.p12 -password pass:mypass
keytool -importkeystore -destkeystore mykeystore.jks -srckeystore my.p12 -srcstoretype PKCS12 -deststorepass mypass -srcstorepass mypass
Create local truststore:
keytool -keystore my_ca.jks -alias myalias -import -file AmazonRootCA1.pem
My code:
public class AWSSessionCredentialsProviderImpl implements AWSSessionCredentialsProvider {
private static final Logger LOGGER = LogManager.getLogger(AWSSessionCredentialsProviderImpl.class.getName());
private final Gson gson = new Gson();
private SdkHttpClient client;
private HttpExecuteRequest request;
private String awsAccessKeyId;
private String awsSecretAccessKeyId;
private String awsSessionToken;
public void init(String clientId) throws IOException, URISyntaxException {
System.setProperty("javax.net.ssl.trustStore", Configuration.KEYSTOREPATH_CA.toAbsolutePath().toString());
System.setProperty("javax.net.ssl.trustStoreType", "jks");
try {
System.setProperty("javax.net.ssl.trustStorePassword", new String(Files.readAllBytes(Configuration.KEYSTOREPATH_CA_PASS)));
} catch (IOException e) {
throw new IOException("Read password of trust store is failed", e);
}
System.setProperty("javax.net.ssl.keyStore", Configuration.KEYSTOREPATH.toAbsolutePath().toString());
System.setProperty("javax.net.ssl.keyStoreType", "jks");
try {
System.setProperty("javax.net.ssl.keyStorePassword", new String(Files.readAllBytes(Configuration.KEYSTOREPATH_PASS)));
} catch (IOException e) {
throw new IOException("Read password of key store is failed", e);
}
client = ApacheHttpClient.builder().build();
SdkHttpRequest httpRequest;
try {
httpRequest = SdkHttpFullRequest.builder()
.method(SdkHttpMethod.GET)
.uri(new URI(Configuration.CLIENT_ENDPOINT))
.putHeader("x-amzn-iot-thingname", clientId)
.build();
} catch (URISyntaxException e) {
throw new URISyntaxException(Configuration.CLIENT_ENDPOINT, "Building URI from client endpoint is failed");
}
request = HttpExecuteRequest.builder()
.request(httpRequest)
.build();
try {
setCredentials();
} catch (IOException e) {
throw new IOException("Set temporary credentials is failed", e);
}
}
#Override
public void refresh() {
try {
setCredentials();
} catch (IOException e) {
LOGGER.error("Refresh session credentials is failed", e);
}
}
#Override
public AWSSessionCredentials getCredentials() {
return new BasicSessionCredentials(awsAccessKeyId, awsSecretAccessKeyId, awsSessionToken);
}
private void setCredentials() throws IOException {
HttpExecuteResponse response = client.prepareRequest(request).call();
String credStr = IoUtils.toUtf8String(response.responseBody().get());
CredentialsJson credJson = gson.fromJson(credStr, CredentialsJson.class);
awsAccessKeyId = credJson.credentials.accessKeyId;
awsSecretAccessKeyId = credJson.credentials.secretAccessKey;
awsSessionToken = credJson.credentials.sessionToken;
}
}
So, I get temporary credentials successfully, but when I use them:
AWSSessionCredentialsProviderImpl credentialsProvider = new AWSSessionCredentialsProviderImpl();
credentialsProvider.init("someid");
s3Client = AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(credentialsProvider)
.build();
s3Client.putObject(request);
I get the following exception:
Caused by:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
I don't understand why I get this exception if I can get temporary credentials successfully.
The problem could be related with many things.
Most likely, your Java program will not be able to establish a trust relationship with the remote peer, probably because the AWS CA is not one of the preconfigured JVM trusted CAs.
I think the best approach you can take to solve the problem is to pass the SdkHttpClient that you already have to the S3 client as well.
Please, be aware that in your sample code you are using AmazonS3ClientBuilder, a AWS Java SDK version 1 class, meanwhile the rest of the code is using AWS SDK v2.
Maybe you can update your code to the latest version of the S3Client and try something like this:
System.setProperty("javax.net.ssl.trustStore", Configuration.KEYSTOREPATH_CA.toAbsolutePath().toString());
System.setProperty("javax.net.ssl.trustStoreType", "jks");
try {
System.setProperty("javax.net.ssl.trustStorePassword", new String(Files.readAllBytes(Configuration.KEYSTOREPATH_CA_PASS)));
} catch (IOException e) {
throw new IOException("Read password of trust store is failed", e);
}
System.setProperty("javax.net.ssl.keyStore", Configuration.KEYSTOREPATH.toAbsolutePath().toString());
System.setProperty("javax.net.ssl.keyStoreType", "jks");
try {
System.setProperty("javax.net.ssl.keyStorePassword", new String(Files.readAllBytes(Configuration.KEYSTOREPATH_PASS)));
} catch (IOException e) {
throw new IOException("Read password of key store is failed", e);
}
SdkHttpClient client = ApacheHttpClient.builder().build();
// The idea is reuse the configured HTTP client, modify it as per your needs
AWSSessionCredentialsProviderImpl credentialsProvider = new AWSSessionCredentialsProviderImpl(client);
credentialsProvider.init("someid");
S3Client s3 = S3Client.builder()
.httpClient(client)
.region(region)
.credentialsProvider(credentialsProvider)
.build();
Please, be sure that your trust store contains the actual SSL certificate. You have the root CA certificate of AWS, but maybe not the corresponding to the actual service.
If necessary, you can obtain the service SSL certificate with something like this:
openssl s_client -connect s3.us-west-1.amazonaws.com:443
Please, change the command according to your region. You need to extract the PEM content from the response.
As indicated in the comments to the answer, another alternative could be unset the System properties established when you obtain your credentials before the invocation of the S3Client:
System.clearProperty("javax.net.ssl.trustStore");
System.clearProperty("javax.net.ssl.trustStorePassword");
System.clearProperty("javax.net.ssl.trustStoreType");
It will provide the AWS SDK with a fresh environment for invoking S3.
I have a case that my users can authenticate the system via client certificates issued by the 3rd party to whom my application trusts. The 3rd party who issues my client certificates provided root and intermediate certificates which I have added to my truststore on the server side. Now, I need to validate my client certificate building chain of trust and also checking OCSP status of the client certificate. So I loaded trusted intermediate and root certificates from my trustore and built cert path as the code below shows these steps:
private KeyStore loadKeyStore() throws KeyStoreException {
KeyStore trustAnchor = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream in = new FileInputStream(trustStorePath)) {
trustAnchor.load(in, trustStorePass.toCharArray());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (CertificateException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return trustAnchor;
}
private PKIXCertPathBuilderResult buildCertPath(List<X509Certificate> certChain) throws KeyStoreException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, CertPathBuilderException {
KeyStore trustAnchor = loadKeyStore();
X509CertSelector certSelector = new X509CertSelector();
certSelector.setCertificate(certChain.get(0));
PKIXBuilderParameters params = new PKIXBuilderParameters(trustAnchor,certSelector);
CertStoreParameters intermediateCerts = new CollectionCertStoreParameters(certChain);
params.addCertStore(CertStore.getInstance("Collection", intermediateCerts));
params.setRevocationEnabled(false);
CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
PKIXCertPathBuilderResult builderResult = (PKIXCertPathBuilderResult) builder.build(params);
return builderResult;
}
Here certChain is array of certs that I obtained from incoming request via the following line and passed it to buildCertPath as List.
X509Certificate[] certArray = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
Then I came accross this repo on the net
https://github.com/nandosola/trantor-certificate-verifier/blob/master/src/main/java/cc/abstra/trantor/security/certificate/ocsp/OCSPVerifier.java
And I am kind of confused. It seems like in this example trust anchor is built from the cert coming through the request not the ones loaded from truststore. Also while doing OCSP status checking I was planning to get my issuer cert from keystore and pass it to the following method
public RevocationStatus validateOCSPStatus(X509Certificate cert, X509Certificate issuerCert) throws OCSPVerifierException, OCSPException, IOException {
LOGGER.info("Starting to validate OCSP status: ");
OCSPReq ocspReq = generateRequest(issuerCert, cert.getSerialNumber());
if (ocspReq == null) throw new OCSPVerifierException(ExceptionEnum.OCSP_Request_Build_Error);
RevocationStatus status = null;
URL url = getOCSPURL(cert);
if (url == null) throw new OCSPVerifierException(ExceptionEnum.OCSP_INVALID_URL_ERROR);
SingleResp[] responses = null;
OCSPResp ocspResp = getOCSPResponse(url, ocspReq);
if (OCSPResponseStatus.SUCCESSFUL == ocspResp.getStatus()) {
BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResp.getResponseObject();
responses = (basicResponse == null) ? null : basicResponse.getResponses();
}
if (responses != null && responses.length == 1) {
SingleResp resp = responses[0];
status = getRevocationStatus(resp);
}
return status;
}
But what I understood from the repo example I am misleading and I might need to get issuerCert(intermediate, root certificates) from request not from my truststore. Though I realize that if certificate is valid, intermediate and root certs issued my client certificate should be the same as the ones provided by 3rd part certificate authority which I also loaded to my truststore, what if my client cert's chain is ok, but in fact it is not the one my server trusts - thus I presume I have to build my trustanchor from my keystore not from the request and also in the method for OCSP status checking issuer cert should be loaded from keystore not the request I am getting from the client or am I wrong?
Now I have three questions:
1) In the buildCertPath method should truststore anchor be built from the certs(cert itself/intermediate/root) coming through the request or loaded from the truststore?
2) In the buildCertPath method should intermediate certs be obtained from the request or truststore?
3)Finally, in the method validateOCSPStatus from where should I get issuer cert? Put it another way, what should be issuer cert - the ones in the truststore or in the request?
I am really lost among lots of examples out there and I'd be really grateful if anyone helps me clarify my questions
If you are using Spring Boot, you should enable clientAuth and let the JVM take care of client authentication and CRL/OCSP checks without you manually checking all of that. Since the client certs are issued by a third party, make sure they are part of your server's JVM's trust store either by adding them to cacerts or using your own trust store and a configuration like this:
-Djavax.net.ssl.trustStore="trust-store.jks"
-Djavax.net.ssl.trustStorePassword="a-great-password"
I believe OCSP checks are disabled by default and you can enable that with:
System.setProperty('com.sun.net.ssl.checkRevocation', 'true')
Security.setProperty('ocsp.enable', 'true')
About the certs, assuming you have something like Root -> intermediate CA -> client, if you trust the intermediate CA, the client needs to only offer their client cert. If you only include Root, they will need to offer intermediate CA along with the client certificate.
I am trying to consume an API from a 3rd party server. The 3rd party sent me an SSL certificated named certificate.p12 which is the cert file which I use to do the handshake. I have created a custom RestTemplate with SSL as follows:
#Configuration
public class CustomRestTemplate {
private static final String PASSWORD = "fake_password";
private static final String RESOURCE_PATH = "keystore/certificate.p12";
private static final String KEY_TYPE = "PKCS12";
#Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) throws Exception {
char[] password = PASSWORD.toCharArray();
SSLContext sslContext = SSLContextBuilder
.create()
// .loadKeyMaterial(keyStore(RESOURCE_PATH, password), password)
.loadKeyMaterial(keyStore(getClass().getClassLoader().getResource(RESOURCE_PATH).getFile(), password), password)
.loadTrustMaterial(null, new TrustSelfSignedStrategy())
.build();
HttpClient client = HttpClients
.custom()
.setSSLContext(sslContext)
.build();
return builder
.requestFactory(() -> new HttpComponentsClientHttpRequestFactory(client))
.build();
}
private KeyStore keyStore(String file, char[] password) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KEY_TYPE);
File key = ResourceUtils.getFile(file);
try (InputStream in = new FileInputStream(key)) {
keyStore.load(in, password);
}
return keyStore;
}
}
I then call the endpoint using the following code:
#Component
#Service
public class TransactionService implements TransactionInterface {
#Autowired
private CustomRestTemplate restTemplate = new CustomRestTemplate();
private static final String BASE_URL = "https://41.x.x.x:xxxx/";
#Override
public List<Transaction> getUnsentTransactions(int connectionId) throws Exception {
HttpEntity<?> httpEntity = new HttpEntity<>(null, new HttpHeaders());
ResponseEntity<Transaction[]> resp = restTemplate
.restTemplate(new RestTemplateBuilder())
.exchange(BASE_URL + "path/end_point/" + connectionId, HttpMethod.GET, httpEntity, Transaction[].class);
return Arrays.asList(resp.getBody());
}
}
I get an the following stacktrace when trying to consume the api:
org.springframework.web.client.ResourceAccessException: I/O error on GET request for \"https://41.x.x.x:xxxx/path/endpoint/parameters\": Certificate for <41.x.x.x> doesn't match any of the subject alternative names: [some_name_here, some_name_here];
I do not have much experience with TLS or SSL certificates. I am really stuck at the moment and hoping I can get some help here. The 3rd party provided me with a testing site where I can test the endpoints and after importing the certificate.p12 file into my browser I can reach the endpoints using their testing site but my Springboot application still does not reach the endpoint.
Do I need to copy the cert into a specific folder? This does not seem like the case because I get a FileNotFoundException if I change the path or filename and I get a password incorrect error if I enter the wrong password for the certificate.p12 file. I tried using Postman to test it but Postman returns the same stacktrace as my web application.
Looking at the information above, am I missing something? Is the keystore not being created during runtime? Do I need to bind the certificate to the JVM or my outgoing request?
Any help would be appreciated.
Thanks
It looks like you are trying to connect to a server which doesn't have a valid name in the certificate. For example, if you are connecting to "stackoverflow.com", the certificate needs that domain in the "subject" or the "subject alternative names" field.
Even a testing site should have a valid certificate, but if that's not possible (as it's a third party site and you can't change it yourself), you can disable the verification using this question
Of course, this should only be done for testing.
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.
I have completed configuring SSL in my local Tomcat.
And the exception was thrown when I call getOutputStream()
public static InputStream send( String uri, Map<String, String> queryString,
Map<String, String> headers, String method, String reqBody) throws IOException
{
String body = (reqBody != null ? reqBody : "");
//URL myURL = new URL(addUrlParam(uri, queryString));
URL myURL = new URL(uri);
HttpURLConnection httpConn = (HttpURLConnection)myURL.openConnection();
httpConn.setRequestMethod(method);
httpConn.setRequestProperty("Content-Length", String.valueOf(body.toString().getBytes().length));
if ( headers != null ) {
for ( String key : headers.keySet() ) {
httpConn.setRequestProperty(key, headers.get(key));
}
}
httpConn.setDoInput(true);
//POST
if (!HTTP_GET.equals(method) || body.length() > 0) {
httpConn.setDoOutput(true);
httpConn.setUseCaches(false); //POST do not use user caches
***httpConn.getOutputStream().write(body.toString().getBytes());***
httpConn.getOutputStream().flush();
}
return httpConn.getInputStream();
}
How can I fix the issue?
Thanks in advance!!
Java requires a valid certification path to a known root CA. If you are trying to access a site with a self-signed certificate you will need to add the CA key for the self-signed cert to your keystore as a CA key. Assuming your CA certificate is in a file cacert.pem, use keytool as follows:
keytool -importcert -file cacert.pem -keystore client.jks -storepass some-password