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