Merge "Filter out SCTs emitted after a log expired" into main am: ff806829af
Original change: https://android-review.googlesource.com/c/platform/external/conscrypt/+/3217616
Change-Id: Ifb57c990807ac9fa5dec30d57045a7d746c04331
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/common/src/main/java/org/conscrypt/ct/LogInfo.java b/common/src/main/java/org/conscrypt/ct/LogInfo.java
index 3b031f6..99c8139 100644
--- a/common/src/main/java/org/conscrypt/ct/LogInfo.java
+++ b/common/src/main/java/org/conscrypt/ct/LogInfo.java
@@ -44,6 +44,7 @@
private final byte[] logId;
private final PublicKey publicKey;
private final int state;
+ private final long stateTimestamp;
private final String description;
private final String url;
private final String operator;
@@ -60,6 +61,7 @@
this.logId = builder.logId;
this.publicKey = builder.publicKey;
this.state = builder.state;
+ this.stateTimestamp = builder.stateTimestamp;
this.description = builder.description;
this.url = builder.url;
this.operator = builder.operator;
@@ -69,6 +71,7 @@
private byte[] logId;
private PublicKey publicKey;
private int state;
+ private long stateTimestamp;
private String description;
private String url;
private String operator;
@@ -85,11 +88,12 @@
return this;
}
- public Builder setState(int state) {
+ public Builder setState(int state, long timestamp) {
if (state < 0 || state > STATE_REJECTED) {
throw new IllegalArgumentException("invalid state value");
}
this.state = state;
+ this.stateTimestamp = timestamp;
return this;
}
@@ -139,6 +143,17 @@
return state;
}
+ public int getStateAt(long when) {
+ if (when >= this.stateTimestamp) {
+ return state;
+ }
+ return STATE_UNKNOWN;
+ }
+
+ public long getStateTimestamp() {
+ return stateTimestamp;
+ }
+
public String getOperator() {
return operator;
}
@@ -155,12 +170,14 @@
LogInfo that = (LogInfo) other;
return this.state == that.state && this.description.equals(that.description)
&& this.url.equals(that.url) && this.operator.equals(that.operator)
+ && this.stateTimestamp == that.stateTimestamp
&& Arrays.equals(this.logId, that.logId);
}
@Override
public int hashCode() {
- return Objects.hash(Arrays.hashCode(logId), description, url, state, operator);
+ return Objects.hash(
+ Arrays.hashCode(logId), description, url, state, stateTimestamp, operator);
}
/**
diff --git a/common/src/main/java/org/conscrypt/ct/VerificationResult.java b/common/src/main/java/org/conscrypt/ct/VerificationResult.java
index 1a486ce..354b16a 100644
--- a/common/src/main/java/org/conscrypt/ct/VerificationResult.java
+++ b/common/src/main/java/org/conscrypt/ct/VerificationResult.java
@@ -21,6 +21,14 @@
import java.util.List;
import org.conscrypt.Internal;
+/**
+ * Container for verified SignedCertificateTimestamp.
+ *
+ * getValidSCTs returns SCTs which were found to match a known log and for
+ * which the signature has been verified. There is no guarantee on the state of
+ * the log (e.g., getLogInfo.getState() may return STATE_UNKNOWN). Further
+ * verification on the compliance with the policy is performed in PolicyImpl.
+ */
@Internal
public class VerificationResult {
private final ArrayList<VerifiedSCT> validSCTs = new ArrayList<VerifiedSCT>();
diff --git a/common/src/test/java/org/conscrypt/ct/VerifierTest.java b/common/src/test/java/org/conscrypt/ct/VerifierTest.java
index 18cc64b..bd26e2c 100644
--- a/common/src/test/java/org/conscrypt/ct/VerifierTest.java
+++ b/common/src/test/java/org/conscrypt/ct/VerifierTest.java
@@ -54,7 +54,7 @@
.setDescription("Test Log")
.setUrl("http://example.com")
.setOperator("LogOperator")
- .setState(LogInfo.STATE_USABLE)
+ .setState(LogInfo.STATE_USABLE, 1643709600000L)
.build();
LogStore store = new LogStore() {
@Override
diff --git a/platform/src/main/java/org/conscrypt/ct/LogStoreImpl.java b/platform/src/main/java/org/conscrypt/ct/LogStoreImpl.java
index 8ab4efb..f9f24ac 100644
--- a/platform/src/main/java/org/conscrypt/ct/LogStoreImpl.java
+++ b/platform/src/main/java/org/conscrypt/ct/LogStoreImpl.java
@@ -19,6 +19,13 @@
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
+import org.conscrypt.ByteArray;
+import org.conscrypt.Internal;
+import org.conscrypt.OpenSSLKey;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
@@ -28,19 +35,17 @@
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
+import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
-import org.conscrypt.ByteArray;
-import org.conscrypt.Internal;
-import org.conscrypt.OpenSSLKey;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
@Internal
public class LogStoreImpl implements LogStore {
@@ -139,7 +144,10 @@
JSONObject stateObject = log.optJSONObject("state");
if (stateObject != null) {
- builder.setState(parseState(stateObject.keys().next()));
+ String state = stateObject.keys().next();
+ String stateTimestamp =
+ stateObject.getJSONObject(state).getString("timestamp");
+ builder.setState(parseState(state), parseStateTimestamp(stateTimestamp));
}
LogInfo logInfo = builder.build();
@@ -180,6 +188,19 @@
}
}
+ // ISO 8601
+ private static DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
+
+ @SuppressWarnings("JavaUtilDate")
+ private static long parseStateTimestamp(String timestamp) {
+ try {
+ Date date = dateFormatter.parse(timestamp);
+ return date.getTime();
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
private static PublicKey parsePubKey(String key) {
byte[] pem = ("-----BEGIN PUBLIC KEY-----\n" + key + "\n-----END PUBLIC KEY-----")
.getBytes(US_ASCII);
diff --git a/platform/src/main/java/org/conscrypt/ct/PolicyImpl.java b/platform/src/main/java/org/conscrypt/ct/PolicyImpl.java
index a6aca5f..d453228 100644
--- a/platform/src/main/java/org/conscrypt/ct/PolicyImpl.java
+++ b/platform/src/main/java/org/conscrypt/ct/PolicyImpl.java
@@ -16,20 +16,38 @@
package org.conscrypt.ct;
+import org.conscrypt.Internal;
+
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
-import org.conscrypt.Internal;
@Internal
public class PolicyImpl implements Policy {
@Override
public PolicyCompliance doesResultConformToPolicy(
VerificationResult result, X509Certificate leaf) {
+ long now = System.currentTimeMillis();
+ return doesResultConformToPolicyAt(result, leaf, now);
+ }
+
+ public PolicyCompliance doesResultConformToPolicyAt(
+ VerificationResult result, X509Certificate leaf, long atTime) {
+ List<VerifiedSCT> validSCTs = new ArrayList<VerifiedSCT>(result.getValidSCTs());
+ /* While the log list supports logs without a state, these entries are
+ * not supported by the log policy. Filter them out. */
+ filterOutUnknown(validSCTs);
+ /* Filter out any SCT issued after a log was retired */
+ filterOutAfterRetired(validSCTs);
+
Set<VerifiedSCT> embeddedValidSCTs = new HashSet<>();
Set<VerifiedSCT> ocspOrTLSValidSCTs = new HashSet<>();
- for (VerifiedSCT vsct : result.getValidSCTs()) {
+ for (VerifiedSCT vsct : validSCTs) {
if (vsct.getSct().getOrigin() == SignedCertificateTimestamp.Origin.EMBEDDED) {
embeddedValidSCTs.add(vsct);
} else {
@@ -37,20 +55,61 @@
}
}
if (embeddedValidSCTs.size() > 0) {
- return conformEmbeddedSCTs(embeddedValidSCTs, leaf);
+ return conformEmbeddedSCTs(embeddedValidSCTs, leaf, atTime);
}
return PolicyCompliance.NOT_ENOUGH_SCTS;
}
+ private void filterOutUnknown(List<VerifiedSCT> scts) {
+ Iterator<VerifiedSCT> it = scts.iterator();
+ while (it.hasNext()) {
+ VerifiedSCT vsct = it.next();
+ if (vsct.getLogInfo().getState() == LogInfo.STATE_UNKNOWN) {
+ it.remove();
+ }
+ }
+ }
+
+ private void filterOutAfterRetired(List<VerifiedSCT> scts) {
+ /* From the policy:
+ *
+ * In order to contribute to a certificate’s CT Compliance, an SCT must
+ * have been issued before the Log’s Retired timestamp, if one exists.
+ * Chrome uses the earliest SCT among all SCTs presented to evaluate CT
+ * compliance against CT Log Retired timestamps. This accounts for edge
+ * cases in which a CT Log becomes Retired during the process of
+ * submitting certificate logging requests.
+ */
+
+ if (scts.size() < 1) {
+ return;
+ }
+ long minTimestamp = scts.get(0).getSct().getTimestamp();
+ for (VerifiedSCT vsct : scts) {
+ long ts = vsct.getSct().getTimestamp();
+ if (ts < minTimestamp) {
+ minTimestamp = ts;
+ }
+ }
+ Iterator<VerifiedSCT> it = scts.iterator();
+ while (it.hasNext()) {
+ VerifiedSCT vsct = it.next();
+ if (vsct.getLogInfo().getState() == LogInfo.STATE_RETIRED
+ && minTimestamp > vsct.getLogInfo().getStateTimestamp()) {
+ it.remove();
+ }
+ }
+ }
+
private PolicyCompliance conformEmbeddedSCTs(
- Set<VerifiedSCT> embeddedValidSCTs, X509Certificate leaf) {
+ Set<VerifiedSCT> embeddedValidSCTs, X509Certificate leaf, long atTime) {
/* 1. At least one Embedded SCT from a CT Log that was Qualified,
* Usable, or ReadOnly at the time of check;
*/
boolean found = false;
for (VerifiedSCT vsct : embeddedValidSCTs) {
LogInfo log = vsct.getLogInfo();
- switch (log.getState()) {
+ switch (log.getStateAt(atTime)) {
case LogInfo.STATE_QUALIFIED:
case LogInfo.STATE_USABLE:
case LogInfo.STATE_READONLY:
@@ -80,7 +139,7 @@
}
for (VerifiedSCT vsct : embeddedValidSCTs) {
LogInfo log = vsct.getLogInfo();
- switch (log.getState()) {
+ switch (log.getStateAt(atTime)) {
case LogInfo.STATE_QUALIFIED:
case LogInfo.STATE_USABLE:
case LogInfo.STATE_READONLY:
diff --git a/platform/src/test/java/org/conscrypt/ct/LogStoreImplTest.java b/platform/src/test/java/org/conscrypt/ct/LogStoreImplTest.java
index af3a2a1..b798d40 100644
--- a/platform/src/test/java/org/conscrypt/ct/LogStoreImplTest.java
+++ b/platform/src/test/java/org/conscrypt/ct/LogStoreImplTest.java
@@ -127,7 +127,7 @@
.setPublicKey(OpenSSLKey.fromPublicKeyPemInputStream(is).getPublicKey())
.setDescription("Operator 1 'Test2024' log")
.setUrl("https://operator1.example.com/logs/test2024/")
- .setState(LogInfo.STATE_USABLE)
+ .setState(LogInfo.STATE_USABLE, 1667328840000L)
.setOperator("Operator 1")
.build();
byte[] log1Id = Base64.getDecoder().decode("7s3QZNXbGs7FXLedtM0TojKHRny87N7DUUhZRnEftZs=");
diff --git a/platform/src/test/java/org/conscrypt/ct/PolicyImplTest.java b/platform/src/test/java/org/conscrypt/ct/PolicyImplTest.java
index 8c4e33e..7188d7c 100644
--- a/platform/src/test/java/org/conscrypt/ct/PolicyImplTest.java
+++ b/platform/src/test/java/org/conscrypt/ct/PolicyImplTest.java
@@ -33,13 +33,27 @@
@RunWith(JUnit4.class)
public class PolicyImplTest {
+ private static final String OPERATOR1 = "operator 1";
+ private static final String OPERATOR2 = "operator 2";
private static LogInfo usableOp1Log1;
private static LogInfo usableOp1Log2;
- private static LogInfo retiredOp1Log;
+ private static LogInfo retiredOp1LogOld;
+ private static LogInfo retiredOp1LogNew;
private static LogInfo usableOp2Log;
private static LogInfo retiredOp2Log;
private static SignedCertificateTimestamp embeddedSCT;
+ /* Some test dates. By default:
+ * - The verification is occurring in January 2024;
+ * - The SCTs were generated in January 2023; and
+ * - The logs got into their state in January 2022.
+ * Other dates are used to exercise edge cases.
+ */
+ private static final long JAN2024 = 1704103200000L;
+ private static final long JUN2023 = 1672999200000L;
+ private static final long JAN2023 = 1672567200000L;
+ private static final long JAN2022 = 1641031200000L;
+
private static class FakePublicKey implements PublicKey {
static final long serialVersionUID = 1;
final byte[] key;
@@ -73,56 +87,62 @@
usableOp1Log1 = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x01}))
.setUrl("")
- .setOperator("operator 1")
- .setState(LogInfo.STATE_USABLE)
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_USABLE, JAN2022)
.build();
usableOp1Log2 = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x02}))
.setUrl("")
- .setOperator("operator 1")
- .setState(LogInfo.STATE_USABLE)
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_USABLE, JAN2022)
.build();
- retiredOp1Log = new LogInfo.Builder()
- .setPublicKey(new FakePublicKey(new byte[] {0x03}))
- .setUrl("")
- .setOperator("operator 1")
- .setState(LogInfo.STATE_RETIRED)
- .build();
+ retiredOp1LogOld = new LogInfo.Builder()
+ .setPublicKey(new FakePublicKey(new byte[] {0x03}))
+ .setUrl("")
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_RETIRED, JAN2022)
+ .build();
+ retiredOp1LogNew = new LogInfo.Builder()
+ .setPublicKey(new FakePublicKey(new byte[] {0x06}))
+ .setUrl("")
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_RETIRED, JUN2023)
+ .build();
usableOp2Log = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x04}))
.setUrl("")
- .setOperator("operator 2")
- .setState(LogInfo.STATE_USABLE)
+ .setOperator(OPERATOR2)
+ .setState(LogInfo.STATE_USABLE, JAN2022)
.build();
retiredOp2Log = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x05}))
.setUrl("")
- .setOperator("operator 2")
- .setState(LogInfo.STATE_RETIRED)
+ .setOperator(OPERATOR2)
+ .setState(LogInfo.STATE_RETIRED, JAN2022)
.build();
- /* Only the origin of the SCT is used during the evaluation for policy
- * compliance. The signature is validated at the previous step (see
- * the Verifier class).
+ /* The origin of the SCT and its timestamp are used during the
+ * evaluation for policy compliance. The signature is validated at the
+ * previous step (see the Verifier class).
*/
- embeddedSCT = new SignedCertificateTimestamp(SignedCertificateTimestamp.Version.V1, null, 0,
- null, null, SignedCertificateTimestamp.Origin.EMBEDDED);
+ embeddedSCT = new SignedCertificateTimestamp(SignedCertificateTimestamp.Version.V1, null,
+ JAN2023, null, null, SignedCertificateTimestamp.Origin.EMBEDDED);
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void emptyVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerificationResult result = new VerificationResult();
X509Certificate leaf = new FakeX509Certificate();
assertEquals("An empty VerificationResult", PolicyCompliance.NOT_ENOUGH_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void validVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
@@ -140,17 +160,17 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("Two valid SCTs from different operators", PolicyCompliance.COMPLY,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void validWithRetiredVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
- .setLogInfo(retiredOp1Log)
+ .setLogInfo(retiredOp1LogNew)
.build();
VerifiedSCT vsct2 = new VerifiedSCT.Builder(embeddedSCT)
@@ -164,13 +184,37 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("One valid, one retired SCTs from different operators",
- PolicyCompliance.COMPLY, p.doesResultConformToPolicy(result, leaf));
+ PolicyCompliance.COMPLY, p.doesResultConformToPolicyAt(result, leaf, JAN2024));
+ }
+
+ @Test
+ public void invalidWithRetiredVerificationResult() throws Exception {
+ PolicyImpl p = new PolicyImpl();
+
+ VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
+ .setStatus(VerifiedSCT.Status.VALID)
+ .setLogInfo(retiredOp1LogOld)
+ .build();
+
+ VerifiedSCT vsct2 = new VerifiedSCT.Builder(embeddedSCT)
+ .setStatus(VerifiedSCT.Status.VALID)
+ .setLogInfo(usableOp2Log)
+ .build();
+
+ VerificationResult result = new VerificationResult();
+ result.add(vsct1);
+ result.add(vsct2);
+
+ X509Certificate leaf = new FakeX509Certificate();
+ assertEquals("One valid, one retired (before SCT timestamp) SCTs from different operators",
+ PolicyCompliance.NOT_ENOUGH_SCTS,
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void invalidOneSctVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
@@ -182,17 +226,17 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("One valid SCT", PolicyCompliance.NOT_ENOUGH_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void invalidTwoSctsVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
- .setLogInfo(retiredOp1Log)
+ .setLogInfo(retiredOp1LogNew)
.build();
VerifiedSCT vsct2 = new VerifiedSCT.Builder(embeddedSCT)
@@ -206,13 +250,13 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("Two retired SCTs from different operators", PolicyCompliance.NOT_ENOUGH_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void invalidTwoSctsSameOperatorVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
@@ -230,6 +274,6 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("Two SCTs from the same operator", PolicyCompliance.NOT_ENOUGH_DIVERSE_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
}
diff --git a/repackaged/common/src/main/java/com/android/org/conscrypt/ct/LogInfo.java b/repackaged/common/src/main/java/com/android/org/conscrypt/ct/LogInfo.java
index 1d7aa91..c2a8498 100644
--- a/repackaged/common/src/main/java/com/android/org/conscrypt/ct/LogInfo.java
+++ b/repackaged/common/src/main/java/com/android/org/conscrypt/ct/LogInfo.java
@@ -47,6 +47,7 @@
private final byte[] logId;
private final PublicKey publicKey;
private final int state;
+ private final long stateTimestamp;
private final String description;
private final String url;
private final String operator;
@@ -63,6 +64,7 @@
this.logId = builder.logId;
this.publicKey = builder.publicKey;
this.state = builder.state;
+ this.stateTimestamp = builder.stateTimestamp;
this.description = builder.description;
this.url = builder.url;
this.operator = builder.operator;
@@ -75,6 +77,7 @@
private byte[] logId;
private PublicKey publicKey;
private int state;
+ private long stateTimestamp;
private String description;
private String url;
private String operator;
@@ -91,11 +94,12 @@
return this;
}
- public Builder setState(int state) {
+ public Builder setState(int state, long timestamp) {
if (state < 0 || state > STATE_REJECTED) {
throw new IllegalArgumentException("invalid state value");
}
this.state = state;
+ this.stateTimestamp = timestamp;
return this;
}
@@ -145,6 +149,17 @@
return state;
}
+ public int getStateAt(long when) {
+ if (when >= this.stateTimestamp) {
+ return state;
+ }
+ return STATE_UNKNOWN;
+ }
+
+ public long getStateTimestamp() {
+ return stateTimestamp;
+ }
+
public String getOperator() {
return operator;
}
@@ -161,12 +176,14 @@
LogInfo that = (LogInfo) other;
return this.state == that.state && this.description.equals(that.description)
&& this.url.equals(that.url) && this.operator.equals(that.operator)
+ && this.stateTimestamp == that.stateTimestamp
&& Arrays.equals(this.logId, that.logId);
}
@Override
public int hashCode() {
- return Objects.hash(Arrays.hashCode(logId), description, url, state, operator);
+ return Objects.hash(
+ Arrays.hashCode(logId), description, url, state, stateTimestamp, operator);
}
/**
diff --git a/repackaged/common/src/main/java/com/android/org/conscrypt/ct/VerificationResult.java b/repackaged/common/src/main/java/com/android/org/conscrypt/ct/VerificationResult.java
index a5af50b..7a2e5df 100644
--- a/repackaged/common/src/main/java/com/android/org/conscrypt/ct/VerificationResult.java
+++ b/repackaged/common/src/main/java/com/android/org/conscrypt/ct/VerificationResult.java
@@ -23,6 +23,12 @@
import com.android.org.conscrypt.Internal;
/**
+ * Container for verified SignedCertificateTimestamp.
+ *
+ * getValidSCTs returns SCTs which were found to match a known log and for
+ * which the signature has been verified. There is no guarantee on the state of
+ * the log (e.g., getLogInfo.getState() may return STATE_UNKNOWN). Further
+ * verification on the compliance with the policy is performed in PolicyImpl.
* @hide This class is not part of the Android public SDK API
*/
@Internal
diff --git a/repackaged/common/src/test/java/com/android/org/conscrypt/ct/VerifierTest.java b/repackaged/common/src/test/java/com/android/org/conscrypt/ct/VerifierTest.java
index 5e317ec..987177b 100644
--- a/repackaged/common/src/test/java/com/android/org/conscrypt/ct/VerifierTest.java
+++ b/repackaged/common/src/test/java/com/android/org/conscrypt/ct/VerifierTest.java
@@ -60,7 +60,7 @@
.setDescription("Test Log")
.setUrl("http://example.com")
.setOperator("LogOperator")
- .setState(LogInfo.STATE_USABLE)
+ .setState(LogInfo.STATE_USABLE, 1643709600000L)
.build();
LogStore store = new LogStore() {
@Override
diff --git a/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/LogStoreImpl.java b/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/LogStoreImpl.java
index 60aa161..4edb113 100644
--- a/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/LogStoreImpl.java
+++ b/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/LogStoreImpl.java
@@ -37,9 +37,13 @@
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
+import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
@@ -145,7 +149,10 @@
JSONObject stateObject = log.optJSONObject("state");
if (stateObject != null) {
- builder.setState(parseState(stateObject.keys().next()));
+ String state = stateObject.keys().next();
+ String stateTimestamp =
+ stateObject.getJSONObject(state).getString("timestamp");
+ builder.setState(parseState(state), parseStateTimestamp(stateTimestamp));
}
LogInfo logInfo = builder.build();
@@ -186,6 +193,19 @@
}
}
+ // ISO 8601
+ private static DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
+
+ @SuppressWarnings("JavaUtilDate")
+ private static long parseStateTimestamp(String timestamp) {
+ try {
+ Date date = dateFormatter.parse(timestamp);
+ return date.getTime();
+ } catch (ParseException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
private static PublicKey parsePubKey(String key) {
byte[] pem = ("-----BEGIN PUBLIC KEY-----\n" + key + "\n-----END PUBLIC KEY-----")
.getBytes(US_ASCII);
diff --git a/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/PolicyImpl.java b/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/PolicyImpl.java
index 595a3ca..3f590d2 100644
--- a/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/PolicyImpl.java
+++ b/repackaged/platform/src/main/java/com/android/org/conscrypt/ct/PolicyImpl.java
@@ -20,7 +20,11 @@
import com.android.org.conscrypt.Internal;
import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -32,9 +36,22 @@
@Override
public PolicyCompliance doesResultConformToPolicy(
VerificationResult result, X509Certificate leaf) {
+ long now = System.currentTimeMillis();
+ return doesResultConformToPolicyAt(result, leaf, now);
+ }
+
+ public PolicyCompliance doesResultConformToPolicyAt(
+ VerificationResult result, X509Certificate leaf, long atTime) {
+ List<VerifiedSCT> validSCTs = new ArrayList<VerifiedSCT>(result.getValidSCTs());
+ /* While the log list supports logs without a state, these entries are
+ * not supported by the log policy. Filter them out. */
+ filterOutUnknown(validSCTs);
+ /* Filter out any SCT issued after a log was retired */
+ filterOutAfterRetired(validSCTs);
+
Set<VerifiedSCT> embeddedValidSCTs = new HashSet<>();
Set<VerifiedSCT> ocspOrTLSValidSCTs = new HashSet<>();
- for (VerifiedSCT vsct : result.getValidSCTs()) {
+ for (VerifiedSCT vsct : validSCTs) {
if (vsct.getSct().getOrigin() == SignedCertificateTimestamp.Origin.EMBEDDED) {
embeddedValidSCTs.add(vsct);
} else {
@@ -42,20 +59,61 @@
}
}
if (embeddedValidSCTs.size() > 0) {
- return conformEmbeddedSCTs(embeddedValidSCTs, leaf);
+ return conformEmbeddedSCTs(embeddedValidSCTs, leaf, atTime);
}
return PolicyCompliance.NOT_ENOUGH_SCTS;
}
+ private void filterOutUnknown(List<VerifiedSCT> scts) {
+ Iterator<VerifiedSCT> it = scts.iterator();
+ while (it.hasNext()) {
+ VerifiedSCT vsct = it.next();
+ if (vsct.getLogInfo().getState() == LogInfo.STATE_UNKNOWN) {
+ it.remove();
+ }
+ }
+ }
+
+ private void filterOutAfterRetired(List<VerifiedSCT> scts) {
+ /* From the policy:
+ *
+ * In order to contribute to a certificate’s CT Compliance, an SCT must
+ * have been issued before the Log’s Retired timestamp, if one exists.
+ * Chrome uses the earliest SCT among all SCTs presented to evaluate CT
+ * compliance against CT Log Retired timestamps. This accounts for edge
+ * cases in which a CT Log becomes Retired during the process of
+ * submitting certificate logging requests.
+ */
+
+ if (scts.size() < 1) {
+ return;
+ }
+ long minTimestamp = scts.get(0).getSct().getTimestamp();
+ for (VerifiedSCT vsct : scts) {
+ long ts = vsct.getSct().getTimestamp();
+ if (ts < minTimestamp) {
+ minTimestamp = ts;
+ }
+ }
+ Iterator<VerifiedSCT> it = scts.iterator();
+ while (it.hasNext()) {
+ VerifiedSCT vsct = it.next();
+ if (vsct.getLogInfo().getState() == LogInfo.STATE_RETIRED
+ && minTimestamp > vsct.getLogInfo().getStateTimestamp()) {
+ it.remove();
+ }
+ }
+ }
+
private PolicyCompliance conformEmbeddedSCTs(
- Set<VerifiedSCT> embeddedValidSCTs, X509Certificate leaf) {
+ Set<VerifiedSCT> embeddedValidSCTs, X509Certificate leaf, long atTime) {
/* 1. At least one Embedded SCT from a CT Log that was Qualified,
* Usable, or ReadOnly at the time of check;
*/
boolean found = false;
for (VerifiedSCT vsct : embeddedValidSCTs) {
LogInfo log = vsct.getLogInfo();
- switch (log.getState()) {
+ switch (log.getStateAt(atTime)) {
case LogInfo.STATE_QUALIFIED:
case LogInfo.STATE_USABLE:
case LogInfo.STATE_READONLY:
@@ -85,7 +143,7 @@
}
for (VerifiedSCT vsct : embeddedValidSCTs) {
LogInfo log = vsct.getLogInfo();
- switch (log.getState()) {
+ switch (log.getStateAt(atTime)) {
case LogInfo.STATE_QUALIFIED:
case LogInfo.STATE_USABLE:
case LogInfo.STATE_READONLY:
diff --git a/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/LogStoreImplTest.java b/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/LogStoreImplTest.java
index 58d6b02..4b4387f 100644
--- a/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/LogStoreImplTest.java
+++ b/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/LogStoreImplTest.java
@@ -131,7 +131,7 @@
.setPublicKey(OpenSSLKey.fromPublicKeyPemInputStream(is).getPublicKey())
.setDescription("Operator 1 'Test2024' log")
.setUrl("https://operator1.example.com/logs/test2024/")
- .setState(LogInfo.STATE_USABLE)
+ .setState(LogInfo.STATE_USABLE, 1667328840000L)
.setOperator("Operator 1")
.build();
byte[] log1Id = Base64.getDecoder().decode("7s3QZNXbGs7FXLedtM0TojKHRny87N7DUUhZRnEftZs=");
diff --git a/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/PolicyImplTest.java b/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/PolicyImplTest.java
index 6ad28e2..2ce9c7d 100644
--- a/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/PolicyImplTest.java
+++ b/repackaged/platform/src/test/java/com/android/org/conscrypt/ct/PolicyImplTest.java
@@ -38,13 +38,27 @@
*/
@RunWith(JUnit4.class)
public class PolicyImplTest {
+ private static final String OPERATOR1 = "operator 1";
+ private static final String OPERATOR2 = "operator 2";
private static LogInfo usableOp1Log1;
private static LogInfo usableOp1Log2;
- private static LogInfo retiredOp1Log;
+ private static LogInfo retiredOp1LogOld;
+ private static LogInfo retiredOp1LogNew;
private static LogInfo usableOp2Log;
private static LogInfo retiredOp2Log;
private static SignedCertificateTimestamp embeddedSCT;
+ /* Some test dates. By default:
+ * - The verification is occurring in January 2024;
+ * - The SCTs were generated in January 2023; and
+ * - The logs got into their state in January 2022.
+ * Other dates are used to exercise edge cases.
+ */
+ private static final long JAN2024 = 1704103200000L;
+ private static final long JUN2023 = 1672999200000L;
+ private static final long JAN2023 = 1672567200000L;
+ private static final long JAN2022 = 1641031200000L;
+
private static class FakePublicKey implements PublicKey {
static final long serialVersionUID = 1;
final byte[] key;
@@ -78,56 +92,62 @@
usableOp1Log1 = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x01}))
.setUrl("")
- .setOperator("operator 1")
- .setState(LogInfo.STATE_USABLE)
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_USABLE, JAN2022)
.build();
usableOp1Log2 = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x02}))
.setUrl("")
- .setOperator("operator 1")
- .setState(LogInfo.STATE_USABLE)
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_USABLE, JAN2022)
.build();
- retiredOp1Log = new LogInfo.Builder()
- .setPublicKey(new FakePublicKey(new byte[] {0x03}))
- .setUrl("")
- .setOperator("operator 1")
- .setState(LogInfo.STATE_RETIRED)
- .build();
+ retiredOp1LogOld = new LogInfo.Builder()
+ .setPublicKey(new FakePublicKey(new byte[] {0x03}))
+ .setUrl("")
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_RETIRED, JAN2022)
+ .build();
+ retiredOp1LogNew = new LogInfo.Builder()
+ .setPublicKey(new FakePublicKey(new byte[] {0x06}))
+ .setUrl("")
+ .setOperator(OPERATOR1)
+ .setState(LogInfo.STATE_RETIRED, JUN2023)
+ .build();
usableOp2Log = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x04}))
.setUrl("")
- .setOperator("operator 2")
- .setState(LogInfo.STATE_USABLE)
+ .setOperator(OPERATOR2)
+ .setState(LogInfo.STATE_USABLE, JAN2022)
.build();
retiredOp2Log = new LogInfo.Builder()
.setPublicKey(new FakePublicKey(new byte[] {0x05}))
.setUrl("")
- .setOperator("operator 2")
- .setState(LogInfo.STATE_RETIRED)
+ .setOperator(OPERATOR2)
+ .setState(LogInfo.STATE_RETIRED, JAN2022)
.build();
- /* Only the origin of the SCT is used during the evaluation for policy
- * compliance. The signature is validated at the previous step (see
- * the Verifier class).
+ /* The origin of the SCT and its timestamp are used during the
+ * evaluation for policy compliance. The signature is validated at the
+ * previous step (see the Verifier class).
*/
- embeddedSCT = new SignedCertificateTimestamp(SignedCertificateTimestamp.Version.V1, null, 0,
- null, null, SignedCertificateTimestamp.Origin.EMBEDDED);
+ embeddedSCT = new SignedCertificateTimestamp(SignedCertificateTimestamp.Version.V1, null,
+ JAN2023, null, null, SignedCertificateTimestamp.Origin.EMBEDDED);
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void emptyVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerificationResult result = new VerificationResult();
X509Certificate leaf = new FakeX509Certificate();
assertEquals("An empty VerificationResult", PolicyCompliance.NOT_ENOUGH_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void validVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
@@ -145,17 +165,17 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("Two valid SCTs from different operators", PolicyCompliance.COMPLY,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void validWithRetiredVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
- .setLogInfo(retiredOp1Log)
+ .setLogInfo(retiredOp1LogNew)
.build();
VerifiedSCT vsct2 = new VerifiedSCT.Builder(embeddedSCT)
@@ -169,13 +189,37 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("One valid, one retired SCTs from different operators",
- PolicyCompliance.COMPLY, p.doesResultConformToPolicy(result, leaf));
+ PolicyCompliance.COMPLY, p.doesResultConformToPolicyAt(result, leaf, JAN2024));
+ }
+
+ @Test
+ public void invalidWithRetiredVerificationResult() throws Exception {
+ PolicyImpl p = new PolicyImpl();
+
+ VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
+ .setStatus(VerifiedSCT.Status.VALID)
+ .setLogInfo(retiredOp1LogOld)
+ .build();
+
+ VerifiedSCT vsct2 = new VerifiedSCT.Builder(embeddedSCT)
+ .setStatus(VerifiedSCT.Status.VALID)
+ .setLogInfo(usableOp2Log)
+ .build();
+
+ VerificationResult result = new VerificationResult();
+ result.add(vsct1);
+ result.add(vsct2);
+
+ X509Certificate leaf = new FakeX509Certificate();
+ assertEquals("One valid, one retired (before SCT timestamp) SCTs from different operators",
+ PolicyCompliance.NOT_ENOUGH_SCTS,
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void invalidOneSctVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
@@ -187,17 +231,17 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("One valid SCT", PolicyCompliance.NOT_ENOUGH_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void invalidTwoSctsVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
- .setLogInfo(retiredOp1Log)
+ .setLogInfo(retiredOp1LogNew)
.build();
VerifiedSCT vsct2 = new VerifiedSCT.Builder(embeddedSCT)
@@ -211,13 +255,13 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("Two retired SCTs from different operators", PolicyCompliance.NOT_ENOUGH_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
@Test
@NonCts(reason = NonCtsReasons.INTERNAL_APIS)
public void invalidTwoSctsSameOperatorVerificationResult() throws Exception {
- Policy p = new PolicyImpl();
+ PolicyImpl p = new PolicyImpl();
VerifiedSCT vsct1 = new VerifiedSCT.Builder(embeddedSCT)
.setStatus(VerifiedSCT.Status.VALID)
@@ -235,6 +279,6 @@
X509Certificate leaf = new FakeX509Certificate();
assertEquals("Two SCTs from the same operator", PolicyCompliance.NOT_ENOUGH_DIVERSE_SCTS,
- p.doesResultConformToPolicy(result, leaf));
+ p.doesResultConformToPolicyAt(result, leaf, JAN2024));
}
}