mTLS HTTP Connection
Last updated
Was this helpful?
Last updated
Was this helpful?
This demostrates how to make a mutual TLS (2 Way SSL) http connection using the popular library in Java Maven Project.
Install the following maven dependencies
//pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<dependencies>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
</dependencies>
...
In this example, we are using ca cert, public cert, and elliptic curve key-pair (private key)
The ca cert and public cert should in below format
-----BEGIN CERTIFICATE-----
base64 encoded string
-----END CERTIFICATE-----
The elliptic curve key-pair (private key) should in below format
-----BEGIN EC PARAMETERS-----
base64 encoded string
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
base64 encoded string
-----END EC PRIVATE KEY-----
Example
package com.example.mtls;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
public class Main {
public static void main(String[] args)
{
final String tempPassword = "test";
String publicCaCert = getResourceAsString("ca.crt");
String publicCert = getResourceAsString("client_public.crt");
String privateKey = getResourceAsString("client_private_key_ec.pem");
Certificate caCert = PemUtils.loadCertificate(publicCaCert);
Certificate clientPublicKey = PemUtils.loadCertificate(publicCert);
PrivateKey clientPrivateKey = PemUtils.loadEcPrivateKey(privateKey);
KeyManager[] keyManagers = PemUtils.getKeyManagers(clientPublicKey, clientPrivateKey, tempPassword);
TrustManager[] trustManagers = PemUtils.getTrustManagers(caCert);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, new java.security.SecureRandom());
String invokeUrl = "https://127.0.0.1/some/secure/url/with/mtls";
HttpsURLConnection conn = (HttpsURLConnection) (new URL(invokeUrl)).openConnection();
conn.setSSLSocketFactory(sslContext.getSocketFactory());
conn.setHostnameVerifier(PemUtils.allHostsValid);
conn.setAllowUserInteraction(true);
//conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setConnectTimeout(30000);
conn.setReadTimeout(180000);
conn.setRequestMethod("GET");
conn.setRequestProperty("Content-Type", "application/json");
int responseCode = conn.getResponseCode();
StringBuffer response = new StringBuffer();
BufferedReader in;
if (responseCode == HttpURLConnection.HTTP_OK) { // 200 OK Success
in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
} else {
in = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
}
String inputLine;
while ((inputLine = in.readLine()) != null) {
response.append(inputLine);
}
in.close();
System.out.println(response.toString());
}
public static String getResourceAsString(String fileName) throws IOException {
InputStream inputStream = getResourceAsStream(fileName);
String text = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))
.lines()
.collect(Collectors.joining("\n"));
return text;
}
public static InputStream getResourceAsStream(String fileName) {
// The class loader that loaded the class
ClassLoader classLoader = Main.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream(fileName);
// the stream holding the file content
if (inputStream == null) {
throw new IllegalArgumentException("file not found! " + fileName);
} else {
return inputStream;
}
}
}
The ca cert and public cert should in below format
-----BEGIN CERTIFICATE-----
base64 encoded string
-----END CERTIFICATE-----
The PKCS#8 unencrypted private key should in below format
-----BEGIN PRIVATE KEY-----
base64 encoded string
-----END PRIVATE KEY-----
Example
package com.example.mtls;
...
public class Example {
public static void main(String[] args)
{
final String tempPassword = "test";
String publicCaCert = getResourceAsString("ca.crt");
String publicCert = getResourceAsString("client_public.crt");
String privateKey = getResourceAsString("client_private_key_pkcs8.pem");
Certificate caCert = PemUtils.loadCertificate(publicCaCert);
Certificate clientPublicKey = PemUtils.loadCertificate(publicCert);
PrivateKey clientPrivateKey = PemUtils.loadPkcs8PrivateKey(privateKey);
// same as Example With EC Private Key
...
}
//PemUtils.java
package com.example.mtls;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashSet;
import java.util.Set;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
/**
* <h1>PEM utilities.</h1>
* Reference:
* <ul>
* <li>https://milosgarunovic.com/posts/java-mtls-http-client/</li>
* <li>https://egkatzioura.com/2021/10/20/execute-mtls-calls-using-java/</li>
* <li>https://stackoverflow.com/a/41947163</li>
* <li>https://github.com/spotify/docker-client/blob/master/src/main/java/com/spotify/docker/client/DockerCertificates.java#L130</li>
* <li>https://github.com/Hakky54/sslcontext-kickstart</li>
* <li>http://www.javawenti.com/?post=35759</li>
* <li>https://github.com/aws-samples/serverless-mutual-tls/blob/main/software/3-lambda-using-parameter-store/src/main/java/com/amazon/aws/example/AppClient.java#L61</li>
* </ul>
*
* <h2>Dependencies:</h2>
* <ul>
* <li>
* org.bouncycastle » bcprov-jdk15on » 1.70 <br/>
* https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on
* </li>
* <li>
* org.bouncycastle » bcpkix-jdk15on » 1.70 <br/>
* https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on
* </li>
* </ul>
*/
public class PemUtils {
private static final Set<String> PRIVATE_KEY_ALGS = new HashSet<String>(Arrays.asList("RSA", "EC"));
private static final Logger LOG = LogManager.getLogger(PemUtils.class);
/**
* Get Key Managers by public certificate and private key
* @param clientPublicCert - The public certificate
* @param clientPrivateKey - The private key
* @param keyStorePassword - The keystore password to be set
* @return
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws KeyStoreException
* @throws UnrecoverableKeyException
*/
public static KeyManager[] getKeyManagers(Certificate clientPublicCert, PrivateKey clientPrivateKey,
String keyStorePassword) throws IOException, NoSuchAlgorithmException, CertificateException,
KeyStoreException, UnrecoverableKeyException {
// Create a KeyStore containing public cert and private key
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null);
char[] password = keyStorePassword.toCharArray();
Certificate[] chain = new Certificate[] { clientPublicCert };
// keyStore.setCertificateEntry("client-cert", clientPublicKey);
keyStore.setKeyEntry("client-key", clientPrivateKey, password, chain);
// Create a KeyManager that trusts the keystore
String kmfAlgorithm = KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm);
kmf.init(keyStore, password);
return kmf.getKeyManagers();
}
/**
* Get Trust Managers by CA Certificate
* @param caCert
* @return
* @throws KeyStoreException
* @throws NoSuchAlgorithmException
* @throws CertificateException
* @throws IOException
*/
public static TrustManager[] getTrustManagers(Certificate caCert)
throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
// Create a KeyStore containing our trusted CAs
KeyStore trustKeyStore = KeyStore.getInstance("JKS");
trustKeyStore.load(null, null);
trustKeyStore.setCertificateEntry("ca-cert", caCert);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(trustKeyStore);
return tmf.getTrustManagers();
}
/**
* Read certificate from PEM content
*/
public static Certificate loadCertificate(String certificatePem) throws IOException, GeneralSecurityException {
final byte[] content = readPemContent(certificatePem);
return loadCertificate(content);
}
/**
* Read certificate from bytes
*/
public static Certificate loadCertificate(byte[] certBytes) throws CertificateException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
return certificateFactory.generateCertificate(new ByteArrayInputStream(certBytes));
}
/**
* Read PEM byte content from string
* @param pem
* @return
* @throws IOException
*/
public static byte[] readPemContent(String pem) throws IOException {
final byte[] content;
PemReader pemReader = new PemReader(new StringReader(pem));
final PemObject pemObject = pemReader.readPemObject();
content = pemObject.getContent();
pemReader.close();
return content;
}
/**
* Read PrivateKey from elliptic curve private key PEM content
* <p>Example usage</p>
* <pre>{@code
* String ecPrivateKeyPem = "-----BEGIN EC PRIVATE KEY-----\n"
* + "MHQCAQEEIDzESrZFmTaOozu2NyiS8LMZGqkHfpSOoI/qA9Lw+d4NoAcGBSuBBAAK\n"
* + "oUQDQgAE7kIqoSQzC/UUXdFdQ9Xvu1Lri7pFfd7xDbQWhSqHaDtj+XY36Z1Cznun\n"
* + "GDxlA0AavdVDuoGXxNQPIed3FxPE3Q==\n"
* + "-----END EC PRIVATE KEY-----\n");
* PrivateKey privateKey = loadEcPrivateKey(ecPrivateKeyPem);
* System.out.println(privateKey);
* }</pre>
*
* Refer: <a>https://github.com/spotify/docker-client/blob/9a7c3b4994be9a55988f821f7177029201ff6aef/src/main/java/com/spotify/docker/client/DockerCertificates.java#L130</a>
*/
public static PrivateKey loadEcPrivateKey(String ecPrivateKeyPem) throws IOException {
// removes the EC PARAMETERS footer and header, if any
int beginIndex = ecPrivateKeyPem.indexOf("-----BEGIN EC PRIVATE KEY-----");
String privateKey = ecPrivateKeyPem.substring(beginIndex);
final Reader reader = new StringReader(privateKey);
final PEMParser pemParser = new PEMParser(reader);
try {
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
PEMDecryptorProvider decryptionProv = new JcePEMDecryptorProviderBuilder().build(null);
Object keyPair = pemParser.readObject();
PrivateKeyInfo keyInfo;
if (keyPair instanceof PEMEncryptedKeyPair) {
PEMKeyPair decryptedKeyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProv);
keyInfo = decryptedKeyPair.getPrivateKeyInfo();
LOG.info("load EC Private Key from PEMEncryptedKeyPair");
} else {
keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
LOG.info("load EC Private Key from PEMKeyPair");
}
return converter.getPrivateKey(keyInfo);
} catch (IOException e) {
throw e;
} finally {
reader.close();
pemParser.close();
}
}
/**
* Read PrivateKey from PKCS#8 private key PEM content using "RSA" algorithm,
* retry with "EC" algorithm if failed to read from "RSA" algorithm
*/
public static PrivateKey loadPkcs8PrivateKey(String privateKeyPem)
throws NoSuchAlgorithmException, InvalidKeySpecException {
// removes PKCS#8 footer, header, carriage return, line breaks
String privateString = privateKeyPem
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\r", "")
.replaceAll("\n", "");
// adds BouncyCastle Provider
Security.addProvider(new BouncyCastleProvider());
// decode the content using Base64 decoder
byte[] encoded = Base64.getDecoder().decode(privateString);
// Creates a new PKCS8EncodedKeySpec with the given encoded key.
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
// Try generate PrivateKey from RSA and EC algorithm
return tryGeneratePrivateKey(keySpec, PRIVATE_KEY_ALGS);
}
private static PrivateKey tryGeneratePrivateKey(final PKCS8EncodedKeySpec spec, final Set<String> algorithms)
throws InvalidKeySpecException {
KeyFactory kf;
PrivateKey key;
for (final String algorithm : algorithms) {
try {
kf = KeyFactory.getInstance(algorithm);
key = kf.generatePrivate(spec);
LOG.info("Generated private key from spec using the '{}' algorithm", algorithm);
return key;
} catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
LOG.info("Tried generating private key from spec using the '{}' algorithm", algorithm, e);
}
}
final String error = String.format("Could not generate private key from spec. Tried using %s",
String.join(",", algorithms));
throw new InvalidKeySpecException(error);
}
/**
* Gets a HostVerifier that accepts all connections.
* You should implement your own HostVerifier with custom verify condition if required.
*/
public static HostnameVerifier allHostsValid = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
/**
* Gets a TrustManager that will accept all certificates presented from the server.
* Usually when using self signed certificates we need to use a TrustManager that will accept them.
* You should generate the TrustManager by the CA certificate or keystore if required.
*/
public static TrustManager[] acceptAllTrustManager = {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
}
}
};
}
Check a certificate
openssl x509 -in certificate.crt -text -noout
openssl x509 -in certificate.cer -text -noout
Convert elliptic curve key-pair (private key) to PKCS#8 pem format
openssl pkcs8 -topk8 -nocrypt -in client_private_key_ec.pem -out client_private_key_pkcs8.pem
If you having the ".jks" file, it would be more straigh forward to generate the KeyManagers and TrustManager compare to using PEM files in this example.