Java class that allows automated certificate transparency checks
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