ct: Add code to verify timestamps for certificates.

This change only provides the implementation. The verifier is not
invoked during handshake yet.

Change-Id: I4c270e518f0fc678972cbfe5f8da6f46874dc306
diff --git a/src/main/java/org/conscrypt/ct/CTConstants.java b/src/main/java/org/conscrypt/ct/CTConstants.java
index 4f77350..1bf4abc 100644
--- a/src/main/java/org/conscrypt/ct/CTConstants.java
+++ b/src/main/java/org/conscrypt/ct/CTConstants.java
@@ -33,6 +33,9 @@
     public static final int LOG_ENTRY_TYPE_LENGTH = 2;
     public static final int CERTIFICATE_LENGTH_BYTES = 3;
 
+    public static final int SERIALIZED_SCT_LENGTH_BYTES = 2;
+    public static final int SCT_LIST_LENGTH_BYTES = 2;
+
     public static final int ISSUER_KEY_HASH_LENGTH = 32;
 }
 
diff --git a/src/main/java/org/conscrypt/ct/CTLogInfo.java b/src/main/java/org/conscrypt/ct/CTLogInfo.java
new file mode 100644
index 0000000..af951ba
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/CTLogInfo.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import java.util.Arrays;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+
+/**
+ * Properties about a Certificate Transparency Log.
+ * This object stores information about a CT log, its public key, description and URL.
+ * It allows verification of SCTs against the log's public key.
+ */
+public class CTLogInfo {
+    private final byte[] logId;
+    private final PublicKey publicKey;
+    private final String description;
+    private final String url;
+
+    public CTLogInfo(PublicKey publicKey, String description, String url) {
+        try {
+            this.logId = MessageDigest.getInstance("SHA-256")
+                .digest(publicKey.getEncoded());
+        } catch (NoSuchAlgorithmException e) {
+            // SHA-256 is guaranteed to be available
+            throw new RuntimeException(e);
+        }
+
+        this.publicKey = publicKey;
+        this.description = description;
+        this.url = url;
+    }
+
+    /**
+     * Get the log's ID, that is the SHA-256 hash of it's public key
+     */
+    public byte[] getID() {
+        return logId;
+    }
+
+    public PublicKey getPublicKey() {
+        return publicKey;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    /**
+     * Verify the signature of a signed certificate timestamp for the given certificate entry
+     * against the log's public key.
+     *
+     * @return the result of the verification
+     */
+    public VerifiedSCT.Status verifySingleSCT(SignedCertificateTimestamp sct,
+                                              CertificateEntry entry) {
+        if (!Arrays.equals(sct.getLogID(), getID())) {
+            return VerifiedSCT.Status.UNKNOWN_LOG;
+        }
+
+        byte[] toVerify;
+        try {
+            toVerify = sct.encodeTBS(entry);
+        } catch (SerializationException e) {
+            return VerifiedSCT.Status.INVALID_SCT;
+        }
+
+        Signature signature;
+        try {
+            String algorithm = sct.getSignature().getAlgorithm();
+            signature = Signature.getInstance(algorithm);
+        } catch (NoSuchAlgorithmException e) {
+            return VerifiedSCT.Status.INVALID_SCT;
+        }
+
+        try {
+            signature.initVerify(publicKey);
+        } catch (InvalidKeyException e) {
+            return VerifiedSCT.Status.INVALID_SCT;
+        }
+
+        try {
+            signature.update(toVerify);
+            if (!signature.verify(sct.getSignature().getSignature())) {
+                return VerifiedSCT.Status.INVALID_SIGNATURE;
+            }
+            return VerifiedSCT.Status.VALID;
+        } catch (SignatureException e) {
+            // This only happens if the signature is not initialized,
+            // but we call initVerify just before, so it should never do
+            throw new RuntimeException(e);
+        }
+    }
+}
+
diff --git a/src/main/java/org/conscrypt/ct/CTLogStore.java b/src/main/java/org/conscrypt/ct/CTLogStore.java
new file mode 100644
index 0000000..24a0b43
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/CTLogStore.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+public interface CTLogStore {
+    CTLogInfo getKnownLog(byte[] logId);
+}
+
diff --git a/src/main/java/org/conscrypt/ct/CTVerificationResult.java b/src/main/java/org/conscrypt/ct/CTVerificationResult.java
new file mode 100644
index 0000000..e158edb
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/CTVerificationResult.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.ArrayList;
+
+public class CTVerificationResult {
+    private final List<VerifiedSCT> validSCTs = new ArrayList();
+    private final List<VerifiedSCT> invalidSCTs = new ArrayList();
+
+    public void add(VerifiedSCT result) {
+        if (result.status == VerifiedSCT.Status.VALID) {
+            validSCTs.add(result);
+        } else {
+            invalidSCTs.add(result);
+        }
+    }
+
+    public List<VerifiedSCT> getValidSCTs() {
+        return Collections.unmodifiableList(validSCTs);
+    }
+
+    public List<VerifiedSCT> getInvalidSCTs() {
+        return Collections.unmodifiableList(invalidSCTs);
+    }
+}
+
diff --git a/src/main/java/org/conscrypt/ct/CTVerifier.java b/src/main/java/org/conscrypt/ct/CTVerifier.java
new file mode 100644
index 0000000..6f8737d
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/CTVerifier.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import org.conscrypt.NativeCrypto;
+import org.conscrypt.OpenSSLX509Certificate;
+
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class CTVerifier {
+    private final CTLogStore store;
+
+    public CTVerifier(CTLogStore store) {
+        this.store = store;
+    }
+
+    /**
+     * Verify a certificate chain for transparency.
+     * Signed timestamps are extracted from the leaf certificate, TLS extension, and stapled ocsp
+     * response, and verified against the list of known logs.
+     * @throws IllegalArgumentException if the chain is empty
+     */
+    public CTVerificationResult verifySignedCertificateTimestamps(OpenSSLX509Certificate[] chain,
+            byte[] tlsData, byte[] ocspData) throws CertificateEncodingException {
+        if (chain.length == 0) {
+            throw new IllegalArgumentException("Chain of certificates mustn't be empty.");
+        }
+
+        OpenSSLX509Certificate leaf = chain[0];
+
+        CTVerificationResult result = new CTVerificationResult();
+        List<SignedCertificateTimestamp> tlsScts = getSCTsFromTLSExtension(tlsData);
+        verifyExternalSCTs(tlsScts, leaf, result);
+
+        List<SignedCertificateTimestamp> ocspScts = getSCTsFromOCSPResponse(ocspData, chain);
+        verifyExternalSCTs(ocspScts, leaf, result);
+
+        List<SignedCertificateTimestamp> embeddedScts = getSCTsFromX509Extension(chain[0]);
+        verifyEmbeddedSCTs(embeddedScts, chain, result);
+        return result;
+    }
+
+    /**
+     * Verify a list of SCTs which were embedded from an X509 certificate.
+     * The result of the verification for each sct is added to {@code result}.
+     */
+    private void verifyEmbeddedSCTs(List<SignedCertificateTimestamp> scts,
+                                    OpenSSLX509Certificate[] chain,
+                                    CTVerificationResult result) {
+        // Avoid creating the cert entry if we don't need it
+        if (scts.isEmpty()) {
+            return;
+        }
+
+        CertificateEntry precertEntry = null;
+        if (chain.length >= 2) {
+            OpenSSLX509Certificate leaf = chain[0];
+            OpenSSLX509Certificate issuer = chain[1];
+
+            try {
+                precertEntry = CertificateEntry.createForPrecertificate(leaf, issuer);
+            } catch (CertificateException e) {
+                // Leave precertEntry as null, we handle it just below
+            }
+        }
+
+        if (precertEntry == null) {
+            markSCTsAsInvalid(scts, result);
+            return;
+        }
+
+        for (SignedCertificateTimestamp sct: scts) {
+            VerifiedSCT.Status status = verifySingleSCT(sct, precertEntry);
+            result.add(new VerifiedSCT(sct, status));
+        }
+    }
+
+    /**
+     * Verify a list of SCTs which were not embedded in an X509 certificate, that is received
+     * through the TLS or OCSP extensions.
+     * The result of the verification for each sct is added to {@code result}.
+     */
+    private void verifyExternalSCTs(List<SignedCertificateTimestamp> scts,
+                                    OpenSSLX509Certificate leaf,
+                                    CTVerificationResult result) {
+        // Avoid creating the cert entry if we don't need it
+        if (scts.isEmpty()) {
+            return;
+        }
+
+        CertificateEntry x509Entry;
+        try {
+            x509Entry = CertificateEntry.createForX509Certificate(leaf);
+        } catch (CertificateException e) {
+            markSCTsAsInvalid(scts, result);
+            return;
+        }
+
+        for (SignedCertificateTimestamp sct: scts) {
+            VerifiedSCT.Status status = verifySingleSCT(sct, x509Entry);
+            result.add(new VerifiedSCT(sct, status));
+        }
+    }
+
+    /**
+     * Verify a single SCT for the given Certificate Entry
+     */
+    private VerifiedSCT.Status verifySingleSCT(SignedCertificateTimestamp sct,
+                                                         CertificateEntry certEntry) {
+        CTLogInfo log = store.getKnownLog(sct.getLogID());
+        if (log == null) {
+            return VerifiedSCT.Status.UNKNOWN_LOG;
+        }
+
+        return log.verifySingleSCT(sct, certEntry);
+    }
+
+    /**
+     * Add every SCT in {@code scts} to {@code result} with INVALID_SCT as status
+     */
+    private void markSCTsAsInvalid(List<SignedCertificateTimestamp> scts,
+                                   CTVerificationResult result) {
+        for (SignedCertificateTimestamp sct: scts) {
+            result.add(new VerifiedSCT(sct, VerifiedSCT.Status.INVALID_SCT));
+        }
+    }
+
+    /**
+     * Parse an encoded SignedCertificateTimestampList into a list of SignedCertificateTimestamp
+     * instances, as described by RFC6962.
+     * Individual SCTs which fail to be parsed are skipped. If the data is null, or the encompassing
+     * list fails to be parsed, an empty list is returned.
+     * @param origin used to create the SignedCertificateTimestamp instances.
+     */
+    private List<SignedCertificateTimestamp> getSCTsFromSCTList(byte[] data,
+            SignedCertificateTimestamp.Origin origin) {
+        if (data == null) {
+            return Collections.EMPTY_LIST;
+        }
+
+        byte[][] sctList;
+        try {
+            sctList = Serialization.readList(data, CTConstants.SCT_LIST_LENGTH_BYTES,
+                                             CTConstants.SERIALIZED_SCT_LENGTH_BYTES);
+        } catch (SerializationException e) {
+            return Collections.EMPTY_LIST;
+        }
+
+        List<SignedCertificateTimestamp> scts = new ArrayList();
+        for (byte[] encodedSCT: sctList) {
+            try  {
+                SignedCertificateTimestamp sct = SignedCertificateTimestamp.decode(encodedSCT, origin);
+                scts.add(sct);
+            } catch (SerializationException e) {
+                // Ignore errors
+            }
+        }
+
+        return scts;
+    }
+
+    /**
+     * Extract a list of SignedCertificateTimestamp from a TLS "signed_certificate_timestamp"
+     * extension as described by RFC6962.
+     * Individual SCTs which fail to be parsed are skipped. If the data is null, or the encompassing
+     * list fails to be parsed, an empty list is returned.
+     * @param data contents of the TLS extension to be decoded
+     */
+    private List<SignedCertificateTimestamp> getSCTsFromTLSExtension(byte[] data) {
+        return getSCTsFromSCTList(data, SignedCertificateTimestamp.Origin.TLS_EXTENSION);
+    }
+
+    /**
+     * Extract a list of SignedCertificateTimestamp contained in an OCSP response.
+     * If the data is null, or parsing the OCSP response fails, an empty list is returned.
+     * Individual SCTs which fail to be parsed are skipped.
+     * @param data contents of the OCSP response
+     * @param chain certificate chain for which to get SCTs. Must contain at least the leaf and it's
+     *              issuer in order to identify the relevant SingleResponse from the OCSP response,
+     *              or an empty list is returned
+     */
+    private List<SignedCertificateTimestamp> getSCTsFromOCSPResponse(byte[] data,
+            OpenSSLX509Certificate[] chain) {
+        if (data == null || chain.length < 2) {
+            return Collections.EMPTY_LIST;
+        }
+
+        byte[] extData = NativeCrypto.get_ocsp_single_extension(data, CTConstants.OCSP_SCT_LIST_OID,
+                                                                chain[0].getContext(),
+                                                                chain[1].getContext());
+        if (extData == null) {
+            return Collections.EMPTY_LIST;
+        }
+
+        try {
+            return getSCTsFromSCTList(
+                    Serialization.readDEROctetString(
+                      Serialization.readDEROctetString(extData)),
+                    SignedCertificateTimestamp.Origin.OCSP_RESPONSE);
+        } catch (SerializationException e) {
+            return Collections.EMPTY_LIST;
+        }
+    }
+
+    /**
+     * Extract a list of SignedCertificateTimestamp embedded in an X509 certificate.
+     *
+     * If the certificate does not contain any SCT extension, or the encompassing encoded list fails
+     * to be parsed, an empty list is returned. Individual SCTs which fail to be parsed are ignored.
+     */
+    private List<SignedCertificateTimestamp> getSCTsFromX509Extension(OpenSSLX509Certificate leaf) {
+        byte[] extData = leaf.getExtensionValue(CTConstants.X509_SCT_LIST_OID);
+        if (extData == null) {
+            return Collections.EMPTY_LIST;
+        }
+
+        try {
+            return getSCTsFromSCTList(
+                    Serialization.readDEROctetString(
+                      Serialization.readDEROctetString(extData)),
+                    SignedCertificateTimestamp.Origin.EMBEDDED);
+        } catch (SerializationException e) {
+            return Collections.EMPTY_LIST;
+        }
+    }
+}
+
diff --git a/src/main/java/org/conscrypt/ct/Serialization.java b/src/main/java/org/conscrypt/ct/Serialization.java
index 187eb21..5525711 100644
--- a/src/main/java/org/conscrypt/ct/Serialization.java
+++ b/src/main/java/org/conscrypt/ct/Serialization.java
@@ -25,6 +25,37 @@
 public class Serialization {
     private Serialization() {}
 
+    private static final int DER_TAG_MASK = 0x3f;
+    private static final int DER_TAG_OCTET_STRING = 0x4;
+    private static final int DER_LENGTH_LONG_FORM_FLAG = 0x80;
+
+    public static byte[] readDEROctetString(byte[] input)
+            throws SerializationException {
+        return readDEROctetString(new ByteArrayInputStream(input));
+    }
+
+    public static byte[] readDEROctetString(InputStream input)
+            throws SerializationException {
+        int tag = readByte(input) & DER_TAG_MASK;
+        if (tag != DER_TAG_OCTET_STRING) {
+            throw new SerializationException("Wrong DER tag, expected OCTET STRING, got " + tag);
+        }
+        int length;
+        int width = readByte(input);
+        if ((width & DER_LENGTH_LONG_FORM_FLAG) != 0) {
+            length = readNumber(input, width & ~DER_LENGTH_LONG_FORM_FLAG);
+        } else {
+            length = width;
+        }
+
+        return readFixedBytes(input, length);
+    }
+
+    public static byte[][] readList(byte[] input, int listWidth, int elemWidth)
+            throws SerializationException {
+        return readList(new ByteArrayInputStream(input), listWidth, elemWidth);
+    }
+
     /**
      * Read a variable length vector of variable sized elements as described by RFC5246 section 4.3.
      * The vector is prefixed by its total length, in bytes and in big endian format,
diff --git a/src/main/java/org/conscrypt/ct/VerifiedSCT.java b/src/main/java/org/conscrypt/ct/VerifiedSCT.java
new file mode 100644
index 0000000..91936b9
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/VerifiedSCT.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+/**
+ * Verification result for a single SCT.
+ */
+public final class VerifiedSCT {
+    public enum Status {
+        VALID,
+        INVALID_SIGNATURE,
+        UNKNOWN_LOG,
+        INVALID_SCT
+    }
+
+    public final SignedCertificateTimestamp sct;
+    public final Status status;
+
+    public VerifiedSCT(SignedCertificateTimestamp sct, Status status) {
+        this.sct = sct;
+        this.status = status;
+    }
+}
+
diff --git a/src/test/java/org/conscrypt/ct/CTVerifierTest.java b/src/test/java/org/conscrypt/ct/CTVerifierTest.java
new file mode 100644
index 0000000..f679d57
--- /dev/null
+++ b/src/test/java/org/conscrypt/ct/CTVerifierTest.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import org.conscrypt.NativeCrypto;
+import org.conscrypt.OpenSSLKey;
+import org.conscrypt.OpenSSLX509Certificate;
+import junit.framework.TestCase;
+import java.security.PublicKey;
+import java.util.Arrays;
+
+import static org.conscrypt.TestUtils.openTestFile;
+import static org.conscrypt.TestUtils.readTestFile;
+
+public class CTVerifierTest extends TestCase {
+    private OpenSSLX509Certificate ca;
+    private OpenSSLX509Certificate cert;
+    private OpenSSLX509Certificate certEmbedded;
+    private CTVerifier ctVerifier;
+
+    @Override
+    public void setUp() throws Exception {
+        ca = OpenSSLX509Certificate.fromX509PemInputStream(openTestFile("ca-cert.pem"));
+        cert = OpenSSLX509Certificate.fromX509PemInputStream(openTestFile("cert.pem"));
+        certEmbedded = OpenSSLX509Certificate.fromX509PemInputStream(
+                openTestFile("cert-ct-embedded.pem"));
+
+        PublicKey key = OpenSSLKey.fromPublicKeyPemInputStream(
+                openTestFile("ct-server-key-public.pem")).getPublicKey();
+
+        final CTLogInfo log = new CTLogInfo(key, "Test Log", "foo");
+        CTLogStore store = new CTLogStore() {
+            public CTLogInfo getKnownLog(byte[] logId) {
+                if (Arrays.equals(logId, log.getID())) {
+                    return log;
+                } else {
+                    return null;
+                }
+            }
+        };
+
+        ctVerifier = new CTVerifier(store);
+    }
+
+    public void test_verifySignedCertificateTimestamps_withOCSPResponse() throws Exception {
+        // This is only implemented for BoringSSL
+        if (!NativeCrypto.isBoringSSL) {
+            return;
+        }
+
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        byte[] ocspResponse = readTestFile("ocsp-response.der");
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, null, ocspResponse);
+        assertEquals(1, result.getValidSCTs().size());
+        assertEquals(0, result.getInvalidSCTs().size());
+    }
+
+    public void test_verifySignedCertificateTimestamps_withTLSExtension() throws Exception {
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        byte[] tlsExtension = readTestFile("ct-signed-timestamp-list");
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, tlsExtension, null);
+        assertEquals(1, result.getValidSCTs().size());
+        assertEquals(0, result.getInvalidSCTs().size());
+    }
+
+    public void test_verifySignedCertificateTimestamps_withEmbeddedExtension() throws Exception {
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { certEmbedded, ca };
+
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, null, null);
+        assertEquals(1, result.getValidSCTs().size());
+        assertEquals(0, result.getInvalidSCTs().size());
+    }
+
+    public void test_verifySignedCertificateTimestamps_withoutTimestamp() throws Exception {
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, null, null);
+        assertEquals(0, result.getValidSCTs().size());
+        assertEquals(0, result.getInvalidSCTs().size());
+    }
+
+    public void test_verifySignedCertificateTimestamps_withInvalidSignature() throws Exception {
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        byte[] tlsExtension = readTestFile("ct-signed-timestamp-list-invalid");
+
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, tlsExtension, null);
+        assertEquals(0, result.getValidSCTs().size());
+        assertEquals(1, result.getInvalidSCTs().size());
+        assertEquals(VerifiedSCT.Status.INVALID_SIGNATURE,
+                     result.getInvalidSCTs().get(0).status);
+    }
+
+    public void test_verifySignedCertificateTimestamps_withUnknownLog() throws Exception {
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        byte[] tlsExtension = readTestFile("ct-signed-timestamp-list-unknown");
+
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, tlsExtension, null);
+        assertEquals(0, result.getValidSCTs().size());
+        assertEquals(1, result.getInvalidSCTs().size());
+        assertEquals(VerifiedSCT.Status.UNKNOWN_LOG,
+                     result.getInvalidSCTs().get(0).status);
+    }
+
+    public void test_verifySignedCertificateTimestamps_withInvalidEncoding() throws Exception {
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        // Just some garbage data which will fail to deserialize
+        byte[] tlsExtension = new byte[] { 1, 2, 3, 4 };
+
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, tlsExtension, null);
+        assertEquals(0, result.getValidSCTs().size());
+        assertEquals(0, result.getInvalidSCTs().size());
+    }
+
+    public void test_verifySignedCertificateTimestamps_withInvalidOCSPResponse() throws Exception {
+        // This is only implemented for BoringSSL
+        if (!NativeCrypto.isBoringSSL) {
+            return;
+        }
+
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        // Just some garbage data which will fail to deserialize
+        byte[] ocspResponse = new byte[] { 1, 2, 3, 4 };
+
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, null, ocspResponse);
+        assertEquals(0, result.getValidSCTs().size());
+        assertEquals(0, result.getInvalidSCTs().size());
+    }
+
+    public void test_verifySignedCertificateTimestamps_withMultipleTimestamps() throws Exception {
+        // This is only implemented for BoringSSL
+        if (!NativeCrypto.isBoringSSL) {
+            return;
+        }
+
+        OpenSSLX509Certificate[] chain = new OpenSSLX509Certificate[] { cert, ca };
+
+        byte[] tlsExtension = readTestFile("ct-signed-timestamp-list-invalid");
+        byte[] ocspResponse = readTestFile("ocsp-response.der");
+
+        CTVerificationResult result =
+            ctVerifier.verifySignedCertificateTimestamps(chain, tlsExtension, ocspResponse);
+        assertEquals(1, result.getValidSCTs().size());
+        assertEquals(1, result.getInvalidSCTs().size());
+        assertEquals(SignedCertificateTimestamp.Origin.OCSP_RESPONSE,
+                     result.getValidSCTs().get(0).sct.getOrigin());
+        assertEquals(SignedCertificateTimestamp.Origin.TLS_EXTENSION,
+                     result.getInvalidSCTs().get(0).sct.getOrigin());
+    }
+}
+
diff --git a/src/test/resources/README b/src/test/resources/README
new file mode 100644
index 0000000..4a04984
--- /dev/null
+++ b/src/test/resources/README
@@ -0,0 +1,26 @@
+This repository contains data used in various tests :
+- ca-cert.pem: Root CA certificate
+
+- ct-server-key-public.pem: Public Key of a test Certificate Transparency log
+
+- cert.pem / cert-key.pem: Certificate issued by ca-cert.pem, and its private key
+
+- cert-ct-poisoned.pem: Same certificate as cert.pem, but with an extra CT Poison extension
+
+- cert-ct-embedded.pem: Same certificate as cert.pem, but with an embedded signed certificate
+    timestamp signed by the test CT log
+
+- ct-signed-timestamp-list: TLS-encoded SignedCertificateTimestampList containing one SCT for
+    cert.pem signed by the test CT log
+
+- ct-signed-timestamp-list-invalid: TLS-encoded SignedCertificateTimestampList containing one SCT
+    signed by the test CT log, but for another certificate
+
+- ct-signed-timestamp-list-unknown: TLS-encoded SignedCertificateTimestampList containing one SCT
+    for cert.pem, but signed by a different log
+
+- ocsp-response.der: OCSP response for cert.pem, containing an SCT for cert.pem signed by the test
+    CT log
+
+- ocsp-response-sct-extension.der: The extension from ocsp-response.der which contains the SCT
+
diff --git a/src/test/resources/cert-ct-embedded.pem b/src/test/resources/cert-ct-embedded.pem
new file mode 100644
index 0000000..eb6acfe
--- /dev/null
+++ b/src/test/resources/cert-ct-embedded.pem
@@ -0,0 +1,65 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 7 (0x7)
+    Signature Algorithm: sha1WithRSAEncryption
+        Issuer: C=GB, O=Certificate Transparency CA, ST=Wales, L=Erw Wen
+        Validity
+            Not Before: Jun  1 00:00:00 2012 GMT
+            Not After : Jun  1 00:00:00 2022 GMT
+        Subject: C=GB, O=Certificate Transparency, ST=Wales, L=Erw Wen
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (1024 bit)
+                Modulus:
+                    00:be:ef:98:e7:c2:68:77:ae:38:5f:75:32:5a:0c:
+                    1d:32:9b:ed:f1:8f:aa:f4:d7:96:bf:04:7e:b7:e1:
+                    ce:15:c9:5b:a2:f8:0e:e4:58:bd:7d:b8:6f:8a:4b:
+                    25:21:91:a7:9b:d7:00:c3:8e:9c:03:89:b4:5c:d4:
+                    dc:9a:12:0a:b2:1e:0c:b4:1c:d0:e7:28:05:a4:10:
+                    cd:9c:5b:db:5d:49:27:72:6d:af:17:10:f6:01:87:
+                    37:7e:a2:5b:1a:1e:39:ee:d0:b8:81:19:dc:15:4d:
+                    c6:8f:7d:a8:e3:0c:af:15:8a:33:e6:c9:50:9f:4a:
+                    05:b0:14:09:ff:5d:d8:7e:b5
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Key Identifier: 
+                20:31:54:1A:F2:5C:05:FF:D8:65:8B:68:43:79:4F:5E:90:36:F7:B4
+            X509v3 Authority Key Identifier: 
+                keyid:5F:9D:88:0D:C8:73:E6:54:D4:F8:0D:D8:E6:B0:C1:24:B4:47:C3:55
+                DirName:/C=GB/O=Certificate Transparency CA/ST=Wales/L=Erw Wen
+                serial:00
+
+            X509v3 Basic Constraints: 
+                CA:FALSE
+            1.3.6.1.4.1.11129.2.4.2: 
+                .z.x.v........RG.ah2].\yY..........?t.d...=.'.......G0E. H/gQ.5..T6...d.=..AB...E0(....>..!.....:.r.....jh.S.}.A.}....Q.....
+    Signature Algorithm: sha1WithRSAEncryption
+         8a:0c:4b:ef:09:9d:47:92:79:af:a0:a2:8e:68:9f:91:e1:c4:
+         42:1b:e2:d2:69:a2:ea:6c:a4:e8:21:5d:de:dd:ca:15:04:a1:
+         1e:7c:87:c4:b7:7e:80:f0:e9:79:03:52:68:f2:7c:a2:0e:16:
+         68:04:ae:55:6f:31:69:81:f9:6a:39:4a:b7:ab:fd:3e:25:5a:
+         c0:04:45:13:fe:76:57:0c:67:95:ab:e4:70:31:33:d3:03:f8:
+         9f:3a:fa:6b:bc:fc:51:73:19:df:d9:5b:93:42:41:21:1f:63:
+         40:35:c3:d0:78:30:7a:68:c6:07:5a:2e:20:c8:9f:36:b8:91:
+         0c:a0
+-----BEGIN CERTIFICATE-----
+MIIDWTCCAsKgAwIBAgIBBzANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJHQjEk
+MCIGA1UEChMbQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5IENBMQ4wDAYDVQQIEwVX
+YWxlczEQMA4GA1UEBxMHRXJ3IFdlbjAeFw0xMjA2MDEwMDAwMDBaFw0yMjA2MDEw
+MDAwMDBaMFIxCzAJBgNVBAYTAkdCMSEwHwYDVQQKExhDZXJ0aWZpY2F0ZSBUcmFu
+c3BhcmVuY3kxDjAMBgNVBAgTBVdhbGVzMRAwDgYDVQQHEwdFcncgV2VuMIGfMA0G
+CSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+75jnwmh3rjhfdTJaDB0ym+3xj6r015a/
+BH634c4VyVui+A7kWL19uG+KSyUhkaeb1wDDjpwDibRc1NyaEgqyHgy0HNDnKAWk
+EM2cW9tdSSdyba8XEPYBhzd+olsaHjnu0LiBGdwVTcaPfajjDK8VijPmyVCfSgWw
+FAn/Xdh+tQIDAQABo4IBOjCCATYwHQYDVR0OBBYEFCAxVBryXAX/2GWLaEN5T16Q
+Nve0MH0GA1UdIwR2MHSAFF+diA3Ic+ZU1PgN2OawwSS0R8NVoVmkVzBVMQswCQYD
+VQQGEwJHQjEkMCIGA1UEChMbQ2VydGlmaWNhdGUgVHJhbnNwYXJlbmN5IENBMQ4w
+DAYDVQQIEwVXYWxlczEQMA4GA1UEBxMHRXJ3IFdlboIBADAJBgNVHRMEAjAAMIGK
+BgorBgEEAdZ5AgQCBHwEegB4AHYA3xwuwRUAlFJHqWFoMl3cXHlZ6PfG04j8AC4L
+vT9012QAAAE92yffkwAABAMARzBFAiBIL2dRrzXbplQ2vh/WZA89v5pBQpSVkkUw
+KI+j5eI+BgIhAOTtwNs6xXKx4vXoq2poBlOYfc9BAn3+/6EFUZ2J7b8IMA0GCSqG
+SIb3DQEBBQUAA4GBAIoMS+8JnUeSea+goo5on5HhxEIb4tJpoupspOghXd7dyhUE
+oR58h8S3foDw6XkDUmjyfKIOFmgErlVvMWmB+Wo5Srer/T4lWsAERRP+dlcMZ5Wr
+5HAxM9MD+J86+mu8/FFzGd/ZW5NCQSEfY0A1w9B4MHpoxgdaLiDInza4kQyg
+-----END CERTIFICATE-----
diff --git a/src/test/resources/ct-server-key-public.pem b/src/test/resources/ct-server-key-public.pem
new file mode 100644
index 0000000..c35ce3f
--- /dev/null
+++ b/src/test/resources/ct-server-key-public.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmXg8sUUzwBYaWrRb+V0IopzQ6o3U
+yEJ04r5ZrRXGdpYM8K+hB0pXrGRLI0eeWz+3skXrS0IO83AhA3GpRL6s6w==
+-----END PUBLIC KEY-----
diff --git a/src/test/resources/ct-signed-timestamp-list b/src/test/resources/ct-signed-timestamp-list
new file mode 100644
index 0000000..c1978a7
--- /dev/null
+++ b/src/test/resources/ct-signed-timestamp-list
Binary files differ
diff --git a/src/test/resources/ct-signed-timestamp-list-invalid b/src/test/resources/ct-signed-timestamp-list-invalid
new file mode 100644
index 0000000..c36abc4
--- /dev/null
+++ b/src/test/resources/ct-signed-timestamp-list-invalid
Binary files differ
diff --git a/src/test/resources/ct-signed-timestamp-list-unknown b/src/test/resources/ct-signed-timestamp-list-unknown
new file mode 100644
index 0000000..26a4153
--- /dev/null
+++ b/src/test/resources/ct-signed-timestamp-list-unknown
Binary files differ