Created
January 13, 2019 18:36
-
-
Save Glamdring/c45cc06cb2e8a2d20badc4473e2e3772 to your computer and use it in GitHub Desktop.
Java class that allows automated certificate transparency checks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package bg.bozho; | |
import java.io.FileInputStream; | |
import java.lang.reflect.Field; | |
import java.net.URL; | |
import java.security.KeyFactory; | |
import java.security.KeyStore; | |
import java.security.PublicKey; | |
import java.security.SecureRandom; | |
import java.security.Security; | |
import java.security.cert.Certificate; | |
import java.security.cert.X509Certificate; | |
import java.security.spec.RSAPublicKeySpec; | |
import java.util.Base64; | |
import java.util.HashMap; | |
import java.util.Map; | |
import javax.net.ssl.HostnameVerifier; | |
import javax.net.ssl.HttpsURLConnection; | |
import javax.net.ssl.SSLContext; | |
import javax.net.ssl.SSLPeerUnverifiedException; | |
import javax.net.ssl.SSLSession; | |
import javax.net.ssl.TrustManager; | |
import org.apache.commons.codec.digest.DigestUtils; | |
import org.bouncycastle.crypto.params.AsymmetricKeyParameter; | |
import org.bouncycastle.crypto.params.ECPublicKeyParameters; | |
import org.bouncycastle.crypto.params.RSAKeyParameters; | |
import org.bouncycastle.crypto.util.PublicKeyFactory; | |
import org.bouncycastle.jce.provider.BouncyCastleProvider; | |
import org.bouncycastle.jce.spec.ECParameterSpec; | |
import org.bouncycastle.jce.spec.ECPublicKeySpec; | |
import org.certificatetransparency.ctlog.SignedTreeHead; | |
import org.certificatetransparency.ctlog.comm.HttpLogClient; | |
import org.conscrypt.Conscrypt; | |
import org.conscrypt.TrustManagerImpl; | |
import org.conscrypt.ct.CTLogInfo; | |
import org.conscrypt.ct.CTLogStore; | |
import org.conscrypt.ct.CTPolicy; | |
import org.conscrypt.ct.CTVerificationResult; | |
import com.fasterxml.jackson.databind.JsonNode; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.fasterxml.jackson.databind.node.ArrayNode; | |
public class CertificateTransparencyVerification { | |
public static void main(String[] args) throws Exception { | |
// Configure Conscrypt to enable and enforce certificate transparencey checks | |
Security.setProperty("conscrypt.ct.enable", "true"); | |
Security.setProperty("conscrypt.ct.enforce.*", "true"); | |
Security.addProvider(new BouncyCastleProvider()); | |
LogStore logStore = new LogStore(); | |
logStore.init("https://www.gstatic.com/ct/log_list/log_list.json"); | |
Security.insertProviderAt(Conscrypt.newProvider(), 1); | |
SSLContext ctx = SSLContext.getInstance("TLS"); | |
KeyStore trustStore = KeyStore.getInstance("JKS"); | |
trustStore.load(new FileInputStream(System.getenv("JAVA_HOME") + "/lib/security/cacerts"), "changeit".toCharArray()); | |
ctx.init(null, new TrustManager[] {new TrustManagerImpl( | |
trustStore, null, null, null, logStore, null, | |
new StrictCTPolicy())}, new SecureRandom()); | |
URL url = new URL("https://google.com"); | |
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); | |
conn.setSSLSocketFactory(ctx.getSocketFactory()); | |
conn.connect(); | |
conn.getInputStream(); | |
conn.disconnect(); | |
} | |
public static class LogStore implements CTLogStore { | |
private Map<String, CTLogInfo> logs = new HashMap<>(); | |
public void init(String url) throws Exception { | |
ObjectMapper mapper = new ObjectMapper(); | |
JsonNode root = mapper.readTree(new URL(url).openStream()); | |
ArrayNode logs = (ArrayNode) root.get("logs"); | |
Field field = CTLogInfo.class.getDeclaredField("logId"); | |
field.setAccessible(true); | |
for (JsonNode log : logs) { | |
String logUrl = log.get("url").asText(); | |
String key = log.get("key").asText(); | |
String description = log.get("description").asText(); | |
try { | |
CTLogInfo logInfo = new CTLogInfo(getKey(key), description, logUrl); | |
// reflection needed, because the CTLogInfo caculates the logID incorrectly | |
field.set(logInfo, DigestUtils.sha256(Base64.getDecoder().decode(key))); | |
this.logs.put(Base64.getEncoder().encodeToString(logInfo.getID()), logInfo); | |
} catch (Exception ex) { | |
ex.printStackTrace(); | |
} | |
} | |
} | |
@Override | |
public CTLogInfo getKnownLog(byte[] logId) { | |
// using base64 form for key, as byte arrays are not fit for that | |
return logs.get(Base64.getEncoder().encodeToString(logId)); | |
} | |
private PublicKey getKey(String key) throws Exception { | |
AsymmetricKeyParameter keyParams = PublicKeyFactory.createKey(Base64.getDecoder().decode(key)); | |
if (keyParams instanceof ECPublicKeyParameters) { | |
ECPublicKeyParameters ecParams = (ECPublicKeyParameters) keyParams; | |
KeyFactory eckf = KeyFactory.getInstance("EC", "BC"); | |
ECParameterSpec spec = new ECParameterSpec(ecParams.getParameters().getCurve(), | |
ecParams.getParameters().getG(), | |
ecParams.getParameters().getN(), | |
ecParams.getParameters().getH()); | |
return eckf.generatePublic(new ECPublicKeySpec(ecParams.getQ(), spec)); | |
} else if (keyParams instanceof RSAKeyParameters) { | |
RSAKeyParameters rsaParams = (RSAKeyParameters) keyParams; | |
KeyFactory rsakf = KeyFactory.getInstance("RSA", "BC"); | |
return rsakf.generatePublic(new RSAPublicKeySpec(rsaParams.getModulus(), rsaParams.getExponent())); | |
} else { | |
return null; | |
} | |
} | |
} | |
public static class StrictCTPolicy implements CTPolicy { | |
@Override | |
public boolean doesResultConformToPolicy(CTVerificationResult result, String hostname, | |
X509Certificate[] chain) { | |
return !result.getValidSCTs().isEmpty() && result.getInvalidSCTs().isEmpty(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment