Skip to content

Commit

Permalink
Improvement: Additional way to handle self-signed certificates if nec…
Browse files Browse the repository at this point in the history
…essary (#401)

* custom self signed cert validation via environment variables based on custom trust store

* optimized caching

---------

Co-authored-by: Szabó Barnabás <[email protected]>
  • Loading branch information
szabarna and Szabó Barnabás authored Nov 16, 2024
1 parent dbbe0a5 commit ad47d42
Showing 1 changed file with 149 additions and 4 deletions.
153 changes: 149 additions & 4 deletions Server/src/main/java/org/openas2/util/HTTPUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.openas2.OpenAS2Exception;
import org.openas2.WrappedException;
import org.openas2.message.Message;
import org.openas2.processor.receiver.AS2ReceiverHandler;

import jakarta.mail.Header;
import jakarta.mail.MessagingException;
Expand All @@ -41,9 +42,13 @@
import java.io.*;
import java.net.*;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;


/**
Expand All @@ -67,6 +72,13 @@ public class HTTPUtil {
public static final String HEADER_USER_AGENT = "User-Agent";
public static final String HEADER_CONNECTION = "Connection";

public static final String SSL_KEYSTORE_PATH_ENV = "SSL_KEYSTORE_PATH";
public static final String SSL_KEYSTORE_PASSWORD_ENV = "SSL_KEYSTORE_PASSWORD";
private static Set<String> cachedFingerprints = ConcurrentHashMap.newKeySet();
private static KeyStore cachedCustomKeyStore = null;

private static final Logger LOG = LoggerFactory.getLogger(AS2ReceiverHandler.class);

public abstract static class Method {
public static final String GET = "GET";
public static final String HEAD = "HEAD";
Expand Down Expand Up @@ -397,37 +409,76 @@ public static ResponseWrapper execRequest(String method, String url, InternetHea
}

private static SSLConnectionSocketFactory buildSslFactory(URL urlObj, Map<String, String> options) throws Exception {


boolean isCustomSelfSignedHandling = isCustomSelfSignedHandling();
boolean overrideSslChecks = "true".equalsIgnoreCase(options.get(HTTPUtil.HTTP_PROP_OVERRIDE_SSL_CHECKS));
SSLContext sslcontext;
String selfSignedCN = System.getProperty("org.openas2.cert.TrustSelfSignedCN");

TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
X509TrustManager defaultTrustManager = null;

if ((selfSignedCN != null && selfSignedCN.contains(urlObj.getHost())) || overrideSslChecks) {

File file = getTrustedCertsKeystore();
KeyStore ks = null;
try (InputStream in = new FileInputStream(file)) {
ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, "changeit".toCharArray());
tmf.init(ks);
}
try {
// Trust own CA and all self-signed certs
sslcontext = SSLContexts.custom().loadTrustMaterial(ks, new TrustSelfSignedStrategy()).build();
defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
// Allow TLSv1 protocol only by default
} catch (Exception e) {
throw new OpenAS2Exception("Self-signed certificate URL connection failed connecting to : " + urlObj.toString(), e);
}
} else if(isCustomSelfSignedHandling) {

KeyStore ks = getCustomKeyStore();
sslcontext = SSLContexts.custom().loadTrustMaterial(ks, new TrustSelfSignedStrategy()).build();
tmf.init(ks);
defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
} else {
sslcontext = SSLContexts.createSystemDefault();
tmf.init((KeyStore) null);
defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
}
// String [] protocols = Properties.getProperty(HTTP_PROP_SSL_PROTOCOLS,
// "TLSv1").split("\\s*,\\s*");
HostnameVerifier hnv = SSLConnectionSocketFactory.getDefaultHostnameVerifier();
SelfSignedTrustManager tm = new SelfSignedTrustManager(defaultTrustManager);

if (overrideSslChecks) {
hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
};
} else if(isCustomSelfSignedHandling) {
hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
try {
// Check if the certificate's fingerprint is cached or if it exists in the custom keystore
X509Certificate[] certs = (X509Certificate[]) session.getPeerCertificates();
String fingerprint = tm.getCertificateFingerprint(certs[0]);

if (cachedFingerprints.contains(fingerprint) || tm.isCertificateInCustomKeystore(certs[0], fingerprint)) {
LOG.info("Hostname verification skipped for trusted certificate: " + certs[0].getSubjectX500Principal().getName());
return true;
}
} catch (Exception e) {
LOG.error("Hostname verification failed: " + e.getMessage(), e);
}

// fallback to default hostname verifier
return SSLConnectionSocketFactory.getDefaultHostnameVerifier().verify(hostname, session);
}
};
}
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, null, null, hnv);
return sslsf;
Expand Down Expand Up @@ -672,6 +723,22 @@ public static HttpURLConnection getConnection(String url, boolean output, boolea
} catch (Exception e) {
throw new OpenAS2Exception("Self-signed certificate URL connection failed connecting to : " + url, e);
}
} else if(isCustomSelfSignedHandling()) {
try {
KeyStore ks = getCustomKeyStore();
SSLContext context = SSLContext.getInstance("TLS");
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
X509TrustManager defaultTrustManager = (X509TrustManager) tmf.getTrustManagers()[0];
SelfSignedTrustManager tm = new SelfSignedTrustManager(defaultTrustManager);
tm.setTrustCN(selfSignedCN);
context.init(null, new TrustManager[]{tm}, null);
connS.setSSLSocketFactory(context.getSocketFactory());

} catch (Exception e) {
throw new OpenAS2Exception("Self-signed certificate URL connection failed connecting to : " + url, e);
}

}
conn = connS;
} else {
Expand All @@ -681,13 +748,13 @@ public static HttpURLConnection getConnection(String url, boolean output, boolea
conn.setDoInput(input);
conn.setUseCaches(useCaches);
conn.setRequestMethod(requestMethod);

return conn;
} catch (IOException ioe) {
throw new WrappedException("URL connection failed connecting to: " + url, ioe);
}
}

private static void setProxyConfig(HttpClientBuilder builder, RequestConfig.Builder rcBuilder, String protocol) throws OpenAS2Exception {
String proxyHost = Properties.getProperty(protocol + ".proxyHost", null);
if (proxyHost == null) {
Expand Down Expand Up @@ -806,6 +873,37 @@ public static void copyHttpHeaders(HttpURLConnection conn, InternetHeaders heade
}
}

private static boolean isCustomSelfSignedHandling() {
return System.getenv(SSL_KEYSTORE_PATH_ENV) != null && System.getenv(SSL_KEYSTORE_PASSWORD_ENV) != null;
}

public static KeyStore getCustomKeyStore() throws OpenAS2Exception {

if (cachedCustomKeyStore != null) {
return cachedCustomKeyStore;
}

String keystorePath = System.getenv(SSL_KEYSTORE_PATH_ENV);
String keystorePassword = System.getenv(SSL_KEYSTORE_PASSWORD_ENV);

LOG.info("Using environment variable for trust store path: " + keystorePath);

if (keystorePath == null) {
throw new OpenAS2Exception("No trust store path set in environment variables");
}

try (InputStream in = new FileInputStream(keystorePath)) {
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(in, keystorePassword.toCharArray());
LOG.info("Trust store loaded successfully from: " + keystorePath);

cachedCustomKeyStore = ks;
return ks;
} catch (Exception e) {
throw new OpenAS2Exception("Failed to load trust store from path: " + keystorePath, e);
}
}

private static class SelfSignedTrustManager implements X509TrustManager {

private final X509TrustManager tm;
Expand All @@ -824,18 +922,65 @@ public void checkClientTrusted(X509Certificate[] chain, String authType) {
}

public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

if (chain.length == 1) {
// Only ignore the check for self signed certs where CN (Canonical Name) matches
// check if certificate is in the truststore or is cached - IF SSL_KEYSTORE_PATH && SSL_KEYSTORE PASSWORD ARE SET
if(isCustomSelfSignedHandling()) {

String fingerprint = getCertificateFingerprint(chain[0]);

// Check if fingerprint is already cached to avoid re-opening keystore
if (cachedFingerprints.contains(fingerprint)) {
LOG.info("Certificate validation passed (cached) for " + chain[0].getSubjectX500Principal().getName());
return;
}

// Proceed with custom keystore handling if not cached
if (isCustomSelfSignedHandling() && isCertificateInCustomKeystore(chain[0], fingerprint)) {
LOG.info("Custom self-signed certificate validation passed for " + chain[0].getSubjectX500Principal().getName());
return;
}
} else {
// Only ignore the check for self signed certs where CN (Canonical Name) matches - DEFAULT BEHAVIOUR
String dn = chain[0].getIssuerX500Principal().getName();
for (int i = 0; i < trustCN.length; i++) {
if (dn.contains("CN=" + trustCN[i])) {
return;
}
}
}
}
tm.checkServerTrusted(chain, authType);
}

private String getCertificateFingerprint(X509Certificate cert) throws CertificateException {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] fingerprintBytes = md.digest(cert.getEncoded());
StringBuilder sb = new StringBuilder();
for (byte b : fingerprintBytes) {
sb.append(String.format("%02X", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
throw new CertificateException("Failed to generate fingerprint for certificate", e);
}
}

private boolean isCertificateInCustomKeystore(X509Certificate cert, String fingerprint) {
try {
KeyStore keyStore = getCustomKeyStore();
String alias = keyStore.getCertificateAlias(cert);
if (alias != null) {
cachedFingerprints.add(fingerprint); // Cache the fingerprint
return true;
}
} catch (Exception e) {
LOG.error("Failed to verify certificate in custom keystore: " + e.getMessage(), e);
}
return false;
}

public void setTrustCN(String trustCN) {
this.trustCN = trustCN.split(",");
}
Expand Down

0 comments on commit ad47d42

Please sign in to comment.