Verify Certificate Transparency Record in Java

 

Page content

A single java file to query Certificate Transparency log records and verification.

Introduction

Certificate Transparency (CT) depends on independent, reliable logs because it is a distributed ecosystem. Built using Merkle trees, logs are publicly verifiable, append-only, and tamper-proof.

Logs are publicly available and monitored.

How CT works

Certificates are deposited in public, transparent logs

Certificate logs are append-only ledgers of certificates. Because they’re distributed and independent, anyone can query them to see what certificates have been included and when. Because they’re append-only, they are verifiable by Monitors. Organizations and individuals with the technical skills and capacity can run a log.

User agents - browsers like Chrome and Safari - help enforce Certificate Transparency.

Logs are cryptographically monitored

Monitors cryptographically check which certificates have been included in logs.

If you subscribe to a CT monitor for your domain, you get updates when precertificates and certificates for those domains are included in any of the logs checked by that monitor. Monitors can be set up and run by anyone

Java Source code to verify Certificate Transparency log record

// Credit: https://gist.github.com/Glamdring/c45cc06cb2e8a2d20badc4473e2e3772
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 transparency 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"); // Change this to any domain.
        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();
        }
    }
}

Dependence

  • Conscrypt – a Java security provider based Google’s fork of OpenSSL
  • BouncyCastle - A lightweight cryptography API for Java.

Reference