Skip to content

Instantly share code, notes, and snippets.

@divergentdave
Last active April 26, 2022 08:13
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save divergentdave/9a68d820e3610513bd4fcdc4ae5f91a1 to your computer and use it in GitHub Desktop.
Save divergentdave/9a68d820e3610513bd4fcdc4ae5f91a1 to your computer and use it in GitHub Desktop.
Ignoring Expired TLS Certificates in Java

Ignoring Expired TLS Certificates in Java

This Gist shows how to make an HTTPS request in Java to a server with an expired HTTPS certificate. Expiration errors are silently ignored for one particular certificate, while all other validation rules are left intact. This whitelist-style behavior minimizes security risk, while still allowing you to communicate with the misconfigured server you are interested in.

Using it

To use this code in your project (go ahead, it's in the public domain!) copy IgnoreExpirationTrustManager into your source tree and adapt Example.buildContext() and Example.runDemo() into your existing network code. When setting up the custom trust manager, pass in the SHA-1 fingerprint of the expired server certificate as the second argument. The SHA-1 fingerprint should be in uppercase hexadecimal, with no colons, spaces, or other separators. Only the server certificate, or leaf certificate, is handled by the code as written, so expired CA certificates will still raise exceptions.

How it works

Dealing with TLS in Java, like many things, quickly runs into the factory-factory-factory problem. Starting from the top level, the demo code creates an SSL context with a custom set of trust managers, and uses the SSL context's socket factory in an HttpsURLConnection. Each X509TrustManager retrieved from the TrustManagerFactory is wrapped by the custom IgnoreExpirationTrustManager before being passed on to the SSLContext. The IgnoreExpirationTrustManager passes through most method calls to the wrapped trust manager, except checkServerTrusted(). Inside checkServerTrusted(), it computes the leaf certificate's fingerprint and compares it to the provided fingerprint of the expected expired certificate. If the fingerprint matches, it wraps the leaf certificate in a custom EternalCertificate, builds a new chain with the wrapped certificate, and then passes the modified chain along for verification by the wrapped trust manager. The EternalCertificate also passes most methods on to its wrapped certificate, except for the two checkValidity() methods. These methods normally check the notBefore and notAfter fields of the certificate against the current time, but instead we return without checking anything. Finally, these overridden methods accomplish our original goal of ignoring certificate expiration.

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
public class Example {
private static class UrlAndFingerprint {
public final String url, fingerprint;
public UrlAndFingerprint(String url, String fingerprint) {
this.url = url;
this.fingerprint = fingerprint;
}
}
public static void main(String[] args) {
UrlAndFingerprint[] examples = new UrlAndFingerprint[] {
new UrlAndFingerprint("https://www.example.com/", "25:09:FB:22:F7:67:1A:EA:2D:0A:28:AE:80:51:6F:39:0D:E0:CA:21".replaceAll(":", "")),
new UrlAndFingerprint("https://expired.badssl.com/", "40:4B:BD:2F:1F:4C:C2:FD:EE:F1:3A:AB:DD:52:3E:F6:1F:1C:71:F3".replaceAll(":", "")),
new UrlAndFingerprint("https://wrong.host.badssl.com/", "69:4C:62:94:2E:B1:F7:6A:C2:02:AC:6C:7C:AA:6F:26:CB:AE:41:80".replaceAll(":", "")),
new UrlAndFingerprint("https://self-signed.badssl.com/", "07:9B:32:59:D0:7C:4D:E2:A1:CE:0E:F4:A5:B5:59:9D:3B:2D:62:EA".replaceAll(":", "")),
new UrlAndFingerprint("https://untrusted-root.badssl.com/", "40:28:A5:C4:43:AB:92:E3:DE:A7:43:1C:59:16:80:32:34:5D:8F:C8".replaceAll(":", ""))
};
for (UrlAndFingerprint example: examples) {
try {
runDemo(example.url, example.fingerprint);
} catch (NoSuchAlgorithmException e) {
System.err.println(e);
} catch (KeyStoreException e) {
System.err.println(e);
} catch (KeyManagementException e) {
System.err.println(e);
} catch (MalformedURLException e) {
System.err.println(e);
} catch (SSLHandshakeException e) {
System.err.println(e);
} catch (IOException e) {
System.err.println(e);
}
}
}
private static void runDemo(String url, String fingerprint) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException, MalformedURLException, IOException {
System.out.printf("%s: ", url);
SSLContext context = buildContext(fingerprint);
URL urlObj = new URL(url);
HttpsURLConnection conn = (HttpsURLConnection) urlObj.openConnection();
conn.setSSLSocketFactory(context.getSocketFactory());
conn.setRequestMethod("HEAD");
System.out.printf("%d\n", conn.getResponseCode());
}
private static SSLContext buildContext(String fingerprint) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
TrustManagerFactory factory;
factory = TrustManagerFactory.getInstance("X509");
factory.init((KeyStore) null);
TrustManager[] trustManagers = factory.getTrustManagers();
for (int i = 0; i < trustManagers.length; i++) {
if (trustManagers[i] instanceof X509TrustManager) {
trustManagers[i] = new IgnoreExpirationTrustManager((X509TrustManager) trustManagers[i], fingerprint);
}
}
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
return sslContext;
}
}
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Date;
import java.util.Set;
import static java.util.Locale.US;
import javax.net.ssl.X509TrustManager;
class IgnoreExpirationTrustManager implements X509TrustManager {
private final X509TrustManager innerTrustManager;
private final String fingerprint;
public IgnoreExpirationTrustManager(X509TrustManager innerTrustManager, String fingerprint) {
this.innerTrustManager = innerTrustManager;
this.fingerprint = fingerprint;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
this.innerTrustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
if (getFingerprint(chain[0]).equals(fingerprint)) {
chain = Arrays.copyOf(chain, chain.length);
X509Certificate[] newChain = new X509Certificate[chain.length];
newChain[0] = new EternalCertificate(chain[0]);
System.arraycopy(chain, 1, newChain, 1, chain.length - 1);
chain = newChain;
}
this.innerTrustManager.checkServerTrusted(chain, authType);
} catch (NoSuchAlgorithmException e) {
throw new CertificateException(e);
}
}
private String getFingerprint(X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
MessageDigest hash = MessageDigest.getInstance("SHA-1");
hash.update(cert.getEncoded());
byte[] fingerprint = hash.digest();
StringBuilder output = new StringBuilder();
for (int i = 0; i < fingerprint.length; i++) {
output.append(String.format(US, "%02X", fingerprint[i]));
}
return output.toString();
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return this.innerTrustManager.getAcceptedIssuers();
}
private class EternalCertificate extends X509Certificate {
private final X509Certificate originalCertificate;
public EternalCertificate(X509Certificate originalCertificate) {
this.originalCertificate = originalCertificate;
}
@Override
public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException {
// Ignore notBefore/notAfter
}
@Override
public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException {
// Ignore notBefore/notAfter
}
@Override
public int getVersion() {
return originalCertificate.getVersion();
}
@Override
public BigInteger getSerialNumber() {
return originalCertificate.getSerialNumber();
}
@Override
public Principal getIssuerDN() {
return originalCertificate.getIssuerDN();
}
@Override
public Principal getSubjectDN() {
return originalCertificate.getSubjectDN();
}
@Override
public Date getNotBefore() {
return originalCertificate.getNotBefore();
}
@Override
public Date getNotAfter() {
return originalCertificate.getNotAfter();
}
@Override
public byte[] getTBSCertificate() throws CertificateEncodingException {
return originalCertificate.getTBSCertificate();
}
@Override
public byte[] getSignature() {
return originalCertificate.getSignature();
}
@Override
public String getSigAlgName() {
return originalCertificate.getSigAlgName();
}
@Override
public String getSigAlgOID() {
return originalCertificate.getSigAlgOID();
}
@Override
public byte[] getSigAlgParams() {
return originalCertificate.getSigAlgParams();
}
@Override
public boolean[] getIssuerUniqueID() {
return originalCertificate.getIssuerUniqueID();
}
@Override
public boolean[] getSubjectUniqueID() {
return originalCertificate.getSubjectUniqueID();
}
@Override
public boolean[] getKeyUsage() {
return originalCertificate.getKeyUsage();
}
@Override
public int getBasicConstraints() {
return originalCertificate.getBasicConstraints();
}
@Override
public byte[] getEncoded() throws CertificateEncodingException {
return originalCertificate.getEncoded();
}
@Override
public void verify(PublicKey key) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
originalCertificate.verify(key);
}
@Override
public void verify(PublicKey key, String sigProvider) throws CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException {
originalCertificate.verify(key, sigProvider);
}
@Override
public String toString() {
return originalCertificate.toString();
}
@Override
public PublicKey getPublicKey() {
return originalCertificate.getPublicKey();
}
@Override
public Set<String> getCriticalExtensionOIDs() {
return originalCertificate.getCriticalExtensionOIDs();
}
@Override
public byte[] getExtensionValue(String oid) {
return originalCertificate.getExtensionValue(oid);
}
@Override
public Set<String> getNonCriticalExtensionOIDs() {
return originalCertificate.getNonCriticalExtensionOIDs();
}
@Override
public boolean hasUnsupportedCriticalExtension() {
return originalCertificate.hasUnsupportedCriticalExtension();
}
}
}
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment