summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Tom Chan <tomchan@google.com> 2025-02-19 21:13:48 +0000
committer Tom Chan <tomchan@google.com> 2025-02-21 19:42:40 +0000
commitcf1648107aab44b4fdcee1a2a96ec9abb6e0c2a7 (patch)
tree3de45f9e24f3eebb107896a83ca9f00096b12bdc
parent3cf302173647ce5c5269358225c2f496bdb34c50 (diff)
Store the entire certificate revocation list locally
Compared to the previous approach which stores previously seen <certificate, last-checked-date> pairs, storing the entire CRL avoids edge cases where a rotated certificate causes an attestation failure because it is not seen before. Test: Manually, also atest AttestationVerificationTest:com.android.server.security.CertificateRevocationStatusManagerTest Bug: 389088384 Flag: EXEMPT bug fix Change-Id: Ia7ae905018d140ff76671d5eb5fc911acaa94897
-rw-r--r--services/core/java/com/android/server/security/CertificateRevocationStatusManager.java281
-rw-r--r--services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java28
-rw-r--r--tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java267
3 files changed, 208 insertions, 368 deletions
diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
index 4cd4b3b84910..799157520ca5 100644
--- a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
+++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java
@@ -24,7 +24,7 @@ import android.content.Context;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.Environment;
-import android.os.PersistableBundle;
+import android.util.AtomicFile;
import android.util.Slog;
import com.android.internal.R;
@@ -35,6 +35,7 @@ import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -42,52 +43,27 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.CertPathValidatorException;
import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeParseException;
+import java.time.OffsetDateTime;
import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
-import java.util.Set;
/** Manages the revocation status of certificates used in remote attestation. */
class CertificateRevocationStatusManager {
private static final String TAG = "AVF_CRL";
// Must be unique within system server
private static final int JOB_ID = 1737671340;
- private static final String REVOCATION_STATUS_FILE_NAME = "certificate_revocation_status.txt";
- private static final String REVOCATION_STATUS_FILE_FIELD_DELIMITER = ",";
+ private static final String REVOCATION_LIST_FILE_NAME = "certificate_revocation_list.json";
+ @VisibleForTesting static final int MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS = 30;
- /**
- * The number of days since last update for which a stored revocation status can be accepted.
- */
- @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30;
-
- @VisibleForTesting static final int NUM_HOURS_BEFORE_NEXT_CHECK = 24;
-
- /**
- * The number of days since issue date for an intermediary certificate to be considered fresh
- * and not require a revocation list check.
- */
- private static final int FRESH_INTERMEDIARY_CERT_DAYS = 70;
-
- /**
- * The expected number of days between a certificate's issue date and notBefore date. Used to
- * infer a certificate's issue date from its notBefore date.
- */
- private static final int DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES = 2;
+ @VisibleForTesting static final int NUM_HOURS_BEFORE_NEXT_FETCH = 24;
private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
private static final Object sFileLock = new Object();
private final Context mContext;
private final String mTestRemoteRevocationListUrl;
- private final File mTestRevocationStatusFile;
+ private final File mTestStoredRevocationListFile;
private final boolean mShouldScheduleJob;
CertificateRevocationStatusManager(Context context) {
@@ -98,29 +74,22 @@ class CertificateRevocationStatusManager {
CertificateRevocationStatusManager(
Context context,
String testRemoteRevocationListUrl,
- File testRevocationStatusFile,
+ File testStoredRevocationListFile,
boolean shouldScheduleJob) {
mContext = context;
mTestRemoteRevocationListUrl = testRemoteRevocationListUrl;
- mTestRevocationStatusFile = testRevocationStatusFile;
+ mTestStoredRevocationListFile = testStoredRevocationListFile;
mShouldScheduleJob = shouldScheduleJob;
}
/**
* Check the revocation status of the provided {@link X509Certificate}s.
*
- * <p>The provided certificates should have been validated and ordered from leaf to a
- * certificate issued by the trust anchor, per the convention specified in the javadoc of {@link
- * java.security.cert.CertPath}.
- *
* @param certificates List of certificates to be checked
* @throws CertPathValidatorException if the check failed
*/
void checkRevocationStatus(List<X509Certificate> certificates)
throws CertPathValidatorException {
- if (!needToCheckRevocationStatus(certificates)) {
- return;
- }
List<String> serialNumbers = new ArrayList<>();
for (X509Certificate certificate : certificates) {
String serialNumber = certificate.getSerialNumber().toString(16);
@@ -129,217 +98,124 @@ class CertificateRevocationStatusManager {
}
serialNumbers.add(serialNumber);
}
+ LocalDateTime now = LocalDateTime.now();
+ JSONObject revocationList;
try {
- if (isLastCheckedWithin(Duration.ofHours(NUM_HOURS_BEFORE_NEXT_CHECK), serialNumbers)) {
- Slog.d(
- TAG,
- "All certificates have been checked for revocation recently. No need to"
- + " check this time.");
+ if (getLastModifiedDateTime(getRevocationListFile())
+ .isAfter(now.minusHours(NUM_HOURS_BEFORE_NEXT_FETCH))) {
+ Slog.d(TAG, "CRL is fetched recently, do not fetch again.");
+ revocationList = getStoredRevocationList();
+ checkRevocationStatus(revocationList, serialNumbers);
return;
}
- } catch (IOException ignored) {
- // Proceed to check the revocation status
+ } catch (IOException | JSONException ignored) {
+ // Proceed to fetch the remote revocation list
}
try {
- JSONObject revocationList = fetchRemoteRevocationList();
- Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
- for (String serialNumber : serialNumbers) {
- areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber));
- }
- updateLastRevocationCheckData(areCertificatesRevoked);
- for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
- if (entry.getValue()) {
- throw new CertPathValidatorException(
- "Certificate " + entry.getKey() + " has been revoked.");
- }
- }
+ byte[] revocationListBytes = fetchRemoteRevocationListBytes();
+ silentlyStoreRevocationList(revocationListBytes);
+ revocationList = parseRevocationList(revocationListBytes);
+ checkRevocationStatus(revocationList, serialNumbers);
} catch (IOException | JSONException ex) {
Slog.d(TAG, "Fallback to check stored revocation status", ex);
if (ex instanceof IOException && mShouldScheduleJob) {
- scheduleJobToUpdateStoredDataWithRemoteRevocationList(serialNumbers);
- }
- for (X509Certificate certificate : certificates) {
- // Assume recently issued certificates are not revoked.
- if (isIssuedWithinDays(certificate, MAX_DAYS_SINCE_LAST_CHECK)) {
- String serialNumber = certificate.getSerialNumber().toString(16);
- serialNumbers.remove(serialNumber);
- }
+ scheduleJobToFetchRemoteRevocationJob();
}
try {
- if (!isLastCheckedWithin(
- Duration.ofDays(MAX_DAYS_SINCE_LAST_CHECK), serialNumbers)) {
- throw new CertPathValidatorException(
- "Unable to verify the revocation status of one of the certificates "
- + serialNumbers);
- }
- } catch (IOException ex2) {
+ revocationList = getStoredRevocationList();
+ checkRevocationStatus(revocationList, serialNumbers);
+ } catch (IOException | JSONException ex2) {
throw new CertPathValidatorException(
- "Unable to load stored revocation status", ex2);
+ "Unable to load or parse stored revocation status", ex2);
}
}
}
- private boolean isLastCheckedWithin(Duration lastCheckedWithin, List<String> serialNumbers)
- throws IOException {
- Map<String, LocalDateTime> lastRevocationCheckData = getLastRevocationCheckData();
+ private static void checkRevocationStatus(JSONObject revocationList, List<String> serialNumbers)
+ throws CertPathValidatorException {
for (String serialNumber : serialNumbers) {
- if (!lastRevocationCheckData.containsKey(serialNumber)
- || lastRevocationCheckData
- .get(serialNumber)
- .isBefore(LocalDateTime.now().minus(lastCheckedWithin))) {
- return false;
+ if (revocationList.has(serialNumber)) {
+ throw new CertPathValidatorException(
+ "Certificate has been revoked: " + serialNumber);
}
}
- return true;
}
- private static boolean needToCheckRevocationStatus(
- List<X509Certificate> certificatesOrderedLeafFirst) {
- if (certificatesOrderedLeafFirst.isEmpty()) {
- return false;
- }
- // A certificate isn't revoked when it is first issued, so we treat it as checked on its
- // issue date.
- if (!isIssuedWithinDays(certificatesOrderedLeafFirst.get(0), MAX_DAYS_SINCE_LAST_CHECK)) {
- return true;
+ private JSONObject getStoredRevocationList() throws IOException, JSONException {
+ File offlineRevocationListFile = getRevocationListFile();
+ if (!offlineRevocationListFile.exists()
+ || isRevocationListExpired(offlineRevocationListFile)) {
+ throw new FileNotFoundException("Offline copy does not exist or has expired.");
}
- for (int i = 1; i < certificatesOrderedLeafFirst.size(); i++) {
- if (!isIssuedWithinDays(
- certificatesOrderedLeafFirst.get(i), FRESH_INTERMEDIARY_CERT_DAYS)) {
- return true;
+ synchronized (sFileLock) {
+ try (FileInputStream inputStream = new FileInputStream(offlineRevocationListFile)) {
+ return parseRevocationList(inputStream.readAllBytes());
}
}
- return false;
}
- private static boolean isIssuedWithinDays(X509Certificate certificate, int days) {
- LocalDate notBeforeDate =
- LocalDate.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
- LocalDate expectedIssueData =
- notBeforeDate.plusDays(DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES);
- return LocalDate.now().minusDays(days + 1).isBefore(expectedIssueData);
+ private boolean isRevocationListExpired(File offlineRevocationListFile) {
+ LocalDateTime acceptableLastModifiedDate =
+ LocalDateTime.now().minusDays(MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS);
+ LocalDateTime lastModifiedDate = getLastModifiedDateTime(offlineRevocationListFile);
+ return lastModifiedDate.isBefore(acceptableLastModifiedDate);
}
- void updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
- JSONObject revocationList, Collection<String> otherCertificatesToCheck) {
- Set<String> allCertificatesToCheck = new HashSet<>(otherCertificatesToCheck);
- try {
- allCertificatesToCheck.addAll(getLastRevocationCheckData().keySet());
- } catch (IOException ex) {
- Slog.e(TAG, "Unable to update last check date of stored data.", ex);
- }
- Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
- for (String serialNumber : allCertificatesToCheck) {
- areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber));
- }
- updateLastRevocationCheckData(areCertificatesRevoked);
+ private static LocalDateTime getLastModifiedDateTime(File file) {
+ // if the file does not exist, file.lastModified() returns 0, so this method returns the
+ // epoch time
+ return LocalDateTime.ofEpochSecond(
+ file.lastModified() / 1000, 0, OffsetDateTime.now().getOffset());
}
/**
- * Update the last revocation check data stored on this device.
+ * Store the provided bytes to the local revocation list file.
+ *
+ * <p>This method does not throw an exception even if it fails to store the bytes.
*
- * @param areCertificatesRevoked A Map whose keys are certificate serial numbers and values are
- * whether that certificate has been revoked
+ * <p>This method internally synchronize file access with other methods in this class.
+ *
+ * @param revocationListBytes The bytes to store to the local revocation list file.
*/
- void updateLastRevocationCheckData(Map<String, Boolean> areCertificatesRevoked) {
- LocalDateTime now = LocalDateTime.now();
+ void silentlyStoreRevocationList(byte[] revocationListBytes) {
synchronized (sFileLock) {
- Map<String, LocalDateTime> lastRevocationCheckData;
+ AtomicFile atomicRevocationListFile = new AtomicFile(getRevocationListFile());
+ FileOutputStream fileOutputStream = null;
try {
- lastRevocationCheckData = getLastRevocationCheckData();
+ fileOutputStream = atomicRevocationListFile.startWrite();
+ fileOutputStream.write(revocationListBytes);
+ atomicRevocationListFile.finishWrite(fileOutputStream);
+ Slog.d(TAG, "Successfully stored revocation list.");
} catch (IOException ex) {
- Slog.e(TAG, "Unable to updateLastRevocationCheckData", ex);
- return;
- }
- for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
- if (entry.getValue()) {
- lastRevocationCheckData.remove(entry.getKey());
- } else {
- lastRevocationCheckData.put(entry.getKey(), now);
+ Slog.e(TAG, "Failed to store the certificate revocation list.", ex);
+ // this happens when fileOutputStream.write fails
+ if (fileOutputStream != null) {
+ atomicRevocationListFile.failWrite(fileOutputStream);
}
}
- storeLastRevocationCheckData(lastRevocationCheckData);
}
}
- Map<String, LocalDateTime> getLastRevocationCheckData() throws IOException {
- Map<String, LocalDateTime> data = new HashMap<>();
- File dataFile = getLastRevocationCheckDataFile();
- synchronized (sFileLock) {
- if (!dataFile.exists()) {
- return data;
- }
- String dataString;
- try (FileInputStream in = new FileInputStream(dataFile)) {
- dataString = new String(in.readAllBytes(), UTF_8);
- }
- for (String line : dataString.split(System.lineSeparator())) {
- String[] elements = line.split(REVOCATION_STATUS_FILE_FIELD_DELIMITER);
- if (elements.length != 2) {
- continue;
- }
- try {
- data.put(elements[0], LocalDateTime.parse(elements[1]));
- } catch (DateTimeParseException ex) {
- Slog.e(
- TAG,
- "Unable to parse last checked LocalDateTime from file. Deleting the"
- + " potentially corrupted file.",
- ex);
- dataFile.delete();
- return data;
- }
- }
+ private File getRevocationListFile() {
+ if (mTestStoredRevocationListFile != null) {
+ return mTestStoredRevocationListFile;
}
- return data;
+ return new File(Environment.getDataSystemDirectory(), REVOCATION_LIST_FILE_NAME);
}
- @VisibleForTesting
- void storeLastRevocationCheckData(Map<String, LocalDateTime> lastRevocationCheckData) {
- StringBuilder dataStringBuilder = new StringBuilder();
- for (Map.Entry<String, LocalDateTime> entry : lastRevocationCheckData.entrySet()) {
- dataStringBuilder
- .append(entry.getKey())
- .append(REVOCATION_STATUS_FILE_FIELD_DELIMITER)
- .append(entry.getValue())
- .append(System.lineSeparator());
- }
- synchronized (sFileLock) {
- try (FileOutputStream fileOutputStream =
- new FileOutputStream(getLastRevocationCheckDataFile())) {
- fileOutputStream.write(dataStringBuilder.toString().getBytes(UTF_8));
- Slog.d(TAG, "Successfully stored revocation status data.");
- } catch (IOException ex) {
- Slog.e(TAG, "Failed to store revocation status data.", ex);
- }
- }
- }
-
- private File getLastRevocationCheckDataFile() {
- if (mTestRevocationStatusFile != null) {
- return mTestRevocationStatusFile;
- }
- return new File(Environment.getDataSystemDirectory(), REVOCATION_STATUS_FILE_NAME);
- }
-
- private void scheduleJobToUpdateStoredDataWithRemoteRevocationList(List<String> serialNumbers) {
+ private void scheduleJobToFetchRemoteRevocationJob() {
JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
if (jobScheduler == null) {
Slog.e(TAG, "Unable to get job scheduler.");
return;
}
Slog.d(TAG, "Scheduling job to fetch remote CRL.");
- PersistableBundle extras = new PersistableBundle();
- extras.putStringArray(
- UpdateCertificateRevocationStatusJobService.EXTRA_KEY_CERTIFICATES_TO_CHECK,
- serialNumbers.toArray(new String[0]));
jobScheduler.schedule(
new JobInfo.Builder(
JOB_ID,
new ComponentName(
mContext,
UpdateCertificateRevocationStatusJobService.class))
- .setExtras(extras)
.setRequiredNetwork(
new NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
@@ -351,13 +227,11 @@ class CertificateRevocationStatusManager {
* Fetches the revocation list from the URL specified in
* R.string.vendor_required_attestation_revocation_list_url
*
- * @return The remote revocation list entries in a JSONObject
+ * @return The remote revocation list entries in a byte[].
* @throws CertPathValidatorException if the URL is not defined or is malformed.
* @throws IOException if the URL is valid but the fetch failed.
- * @throws JSONException if the revocation list content cannot be parsed
*/
- JSONObject fetchRemoteRevocationList()
- throws CertPathValidatorException, IOException, JSONException {
+ byte[] fetchRemoteRevocationListBytes() throws CertPathValidatorException, IOException {
String urlString = getRemoteRevocationListUrl();
if (urlString == null || urlString.isEmpty()) {
throw new CertPathValidatorException(
@@ -369,10 +243,13 @@ class CertificateRevocationStatusManager {
} catch (MalformedURLException ex) {
throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex);
}
- byte[] revocationListBytes;
try (InputStream inputStream = url.openStream()) {
- revocationListBytes = inputStream.readAllBytes();
+ return inputStream.readAllBytes();
}
+ }
+
+ private JSONObject parseRevocationList(byte[] revocationListBytes)
+ throws IOException, JSONException {
JSONObject revocationListJson = new JSONObject(new String(revocationListBytes, UTF_8));
return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
}
diff --git a/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java
index 768c812f47a3..7d1a70eb4094 100644
--- a/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java
+++ b/services/core/java/com/android/server/security/UpdateCertificateRevocationStatusJobService.java
@@ -20,17 +20,15 @@ import android.app.job.JobParameters;
import android.app.job.JobService;
import android.util.Slog;
-import org.json.JSONObject;
-
-import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-/** A {@link JobService} that fetches the certificate revocation list from a remote location. */
+/**
+ * A {@link JobService} that fetches the certificate revocation list from a remote location and
+ * stores it locally.
+ */
public class UpdateCertificateRevocationStatusJobService extends JobService {
- static final String EXTRA_KEY_CERTIFICATES_TO_CHECK =
- "com.android.server.security.extra.CERTIFICATES_TO_CHECK";
private static final String TAG = "AVF_CRL";
private ExecutorService mExecutorService;
@@ -48,20 +46,12 @@ public class UpdateCertificateRevocationStatusJobService extends JobService {
CertificateRevocationStatusManager certificateRevocationStatusManager =
new CertificateRevocationStatusManager(this);
Slog.d(TAG, "Starting to fetch remote CRL from job service.");
- JSONObject revocationList =
- certificateRevocationStatusManager.fetchRemoteRevocationList();
- String[] certificatesToCheckFromJobParams =
- params.getExtras().getStringArray(EXTRA_KEY_CERTIFICATES_TO_CHECK);
- if (certificatesToCheckFromJobParams == null) {
- Slog.e(TAG, "Extras not found: " + EXTRA_KEY_CERTIFICATES_TO_CHECK);
- return;
- }
- certificateRevocationStatusManager
- .updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
- revocationList,
- Arrays.asList(certificatesToCheckFromJobParams));
+ byte[] revocationList =
+ certificateRevocationStatusManager.fetchRemoteRevocationListBytes();
+ certificateRevocationStatusManager.silentlyStoreRevocationList(
+ revocationList);
} catch (Throwable t) {
- Slog.e(TAG, "Unable to update the stored revocation status.", t);
+ Slog.e(TAG, "Unable to update the stored revocation list.", t);
}
jobFinished(params, false);
});
diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
index 586bb76388f6..3854ae6dc9b3 100644
--- a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
+++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java
@@ -20,18 +20,17 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
-import android.os.SystemClock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
-import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.security.cert.CertPathValidatorException;
@@ -39,33 +38,34 @@ import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
@RunWith(AndroidJUnit4.class)
public class CertificateRevocationStatusManagerTest {
private static final String TEST_CERTIFICATE_FILE_1 = "test_attestation_with_root_certs.pem";
private static final String TEST_CERTIFICATE_FILE_2 = "test_attestation_wrong_root_certs.pem";
- private static final String TEST_REVOCATION_LIST_FILE_NAME = "test_revocation_list.json";
+ private static final String TEST_REMOTE_REVOCATION_LIST_FILE_NAME =
+ "test_remote_revocation_list.json";
private static final String REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST =
"test_revocation_list_no_test_certs.json";
private static final String REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST =
"test_revocation_list_with_test_certs.json";
- private static final String TEST_REVOCATION_STATUS_FILE_NAME = "test_revocation_status.txt";
+ private static final String TEST_STORED_REVOCATION_LIST_FILE_NAME =
+ "test_stored_revocation_list.json";
private static final String FILE_URL_PREFIX = "file://";
private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
private CertificateFactory mFactory;
private List<X509Certificate> mCertificates1;
private List<X509Certificate> mCertificates2;
- private File mRevocationListFile;
+ private File mRemoteRevocationListFile;
private String mRevocationListUrl;
private String mNonExistentRevocationListUrl;
- private File mRevocationStatusFile;
+ private File mStoredRevocationListFile;
private CertificateRevocationStatusManager mCertificateRevocationStatusManager;
@Before
@@ -73,27 +73,29 @@ public class CertificateRevocationStatusManagerTest {
mFactory = CertificateFactory.getInstance("X.509");
mCertificates1 = getCertificateChain(TEST_CERTIFICATE_FILE_1);
mCertificates2 = getCertificateChain(TEST_CERTIFICATE_FILE_2);
- mRevocationListFile = new File(mContext.getFilesDir(), TEST_REVOCATION_LIST_FILE_NAME);
- mRevocationListUrl = FILE_URL_PREFIX + mRevocationListFile.getAbsolutePath();
+ mRemoteRevocationListFile =
+ new File(mContext.getFilesDir(), TEST_REMOTE_REVOCATION_LIST_FILE_NAME);
+ mRevocationListUrl = FILE_URL_PREFIX + mRemoteRevocationListFile.getAbsolutePath();
File noSuchFile = new File(mContext.getFilesDir(), "file_does_not_exist");
mNonExistentRevocationListUrl = FILE_URL_PREFIX + noSuchFile.getAbsolutePath();
- mRevocationStatusFile = new File(mContext.getFilesDir(), TEST_REVOCATION_STATUS_FILE_NAME);
+ mStoredRevocationListFile =
+ new File(mContext.getFilesDir(), TEST_STORED_REVOCATION_LIST_FILE_NAME);
}
@After
public void tearDown() throws Exception {
- mRevocationListFile.delete();
- mRevocationStatusFile.delete();
+ mRemoteRevocationListFile.delete();
+ mStoredRevocationListFile.delete();
}
@Test
public void checkRevocationStatus_doesNotExistOnRemoteRevocationList_noException()
throws Exception {
copyFromAssetToFile(
- REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
}
@@ -102,10 +104,10 @@ public class CertificateRevocationStatusManagerTest {
public void checkRevocationStatus_existsOnRemoteRevocationList_throwsException()
throws Exception {
copyFromAssetToFile(
- REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
assertThrows(
CertPathValidatorException.class,
@@ -118,7 +120,7 @@ public class CertificateRevocationStatusManagerTest {
throws Exception {
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false);
assertThrows(
CertPathValidatorException.class,
@@ -128,49 +130,56 @@ public class CertificateRevocationStatusManagerTest {
@Test
public void checkRevocationStatus_savesRevocationStatus() throws Exception {
copyFromAssetToFile(
- REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
- assertThat(mRevocationStatusFile.length()).isGreaterThan(0);
+ assertThat(mStoredRevocationListFile.length()).isGreaterThan(0);
}
@Test
- public void checkRevocationStatus_cannotReachRemoteList_certsSaved_noException()
+ public void checkRevocationStatus_cannotReachRemoteList_listSaved_noException()
throws Exception {
// call checkRevocationStatus once to save the revocation status
copyFromAssetToFile(
- REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
// call checkRevocationStatus again with mNonExistentRevocationListUrl
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false);
mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
}
@Test
- public void checkRevocationStatus_cannotReachRemoteList_someCertsNotSaved_exception()
+ public void checkRevocationStatus_cannotReachRemoteList_storedListTooOld_exception()
throws Exception {
- // call checkRevocationStatus once to save the revocation status for mCertificates2
+ // call checkRevocationStatus once to save the revocation status
copyFromAssetToFile(
- REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
- mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates2);
- // call checkRevocationStatus again with mNonExistentRevocationListUrl, this time for
- // mCertificates1
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // set the last modified date of the stored list to an expired date
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime expiredListDate =
+ now.minusDays(
+ CertificateRevocationStatusManager.MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS
+ + 1);
+ mStoredRevocationListFile.setLastModified(
+ expiredListDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000);
+ // call checkRevocationStatus again with mNonExistentRevocationListUrl
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false);
assertThrows(
CertPathValidatorException.class,
@@ -178,143 +187,112 @@ public class CertificateRevocationStatusManagerTest {
}
@Test
- public void checkRevocationStatus_cannotReachRemoteList_someCertsStatusTooOld_exception()
+ public void checkRevocationStatus_cannotReachRemoteList_storedListIsFresh_noException()
throws Exception {
+ // call checkRevocationStatus once to save the revocation status
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // set the last modified date of the stored list to a barely not expired date
LocalDateTime now = LocalDateTime.now();
- LocalDateTime expiredStatusDate =
- now.minusDays(CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK + 1);
- Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>();
- lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(0)), expiredStatusDate);
- for (int i = 1; i < mCertificates1.size(); i++) {
- lastRevocationCheckData.put(getSerialNumber(mCertificates1.get(i)), now);
- }
- mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData);
-
- assertThrows(
- CertPathValidatorException.class,
- () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
- }
-
- @Test
- public void checkRevocationStatus_cannotReachRemoteList_allCertResultsFresh_noException()
- throws Exception {
+ LocalDateTime barelyFreshDate =
+ now.minusDays(
+ CertificateRevocationStatusManager.MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS
+ - 1);
+ mStoredRevocationListFile.setLastModified(
+ barelyFreshDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000);
+ // call checkRevocationStatus again with mNonExistentRevocationListUrl
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
- LocalDateTime bearlyNotExpiredStatusDate =
- LocalDateTime.now()
- .minusDays(
- CertificateRevocationStatusManager.MAX_DAYS_SINCE_LAST_CHECK - 1);
- Map<String, LocalDateTime> lastRevocationCheckData = new HashMap<>();
- for (X509Certificate certificate : mCertificates1) {
- lastRevocationCheckData.put(getSerialNumber(certificate), bearlyNotExpiredStatusDate);
- }
- mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastRevocationCheckData);
+ mContext, mNonExistentRevocationListUrl, mStoredRevocationListFile, false);
mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
}
@Test
- public void updateLastRevocationCheckData_correctlySavesStatus() throws Exception {
+ public void silentlyStoreRevocationList_storesCorrectly() throws Exception {
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mNonExistentRevocationListUrl, mRevocationStatusFile, false);
- Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
- for (X509Certificate certificate : mCertificates1) {
- areCertificatesRevoked.put(getSerialNumber(certificate), false);
- }
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
+ byte[] revocationList =
+ mCertificateRevocationStatusManager.fetchRemoteRevocationListBytes();
- mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked);
+ mCertificateRevocationStatusManager.silentlyStoreRevocationList(revocationList);
- // no exception
- mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
- // revoke one certificate and try again
- areCertificatesRevoked.put(getSerialNumber(mCertificates1.getLast()), true);
- mCertificateRevocationStatusManager.updateLastRevocationCheckData(areCertificatesRevoked);
- assertThrows(
- CertPathValidatorException.class,
- () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
+ byte[] bytesFromRemoteList;
+ byte[] bytesFromStoredList;
+ try (FileInputStream remoteListInputStream =
+ new FileInputStream(mRemoteRevocationListFile)) {
+ bytesFromRemoteList = remoteListInputStream.readAllBytes();
+ }
+ try (FileInputStream storedListInputStream =
+ new FileInputStream(mStoredRevocationListFile)) {
+ bytesFromStoredList = storedListInputStream.readAllBytes();
+ }
+ assertThat(bytesFromStoredList).isEqualTo(bytesFromRemoteList);
}
@Test
- public void updateLastRevocationCheckDataForAllPreviouslySeenCertificates_updatesCorrectly()
+ public void checkRevocationStatus_recentlyChecked_doesNotFetchRemoteCrl()
throws Exception {
copyFromAssetToFile(
- REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
- // populate the revocation status file
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // indirectly verifies the remote list is not fetched by simulating a remote revocation
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
+
+ // no exception
mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
- // Sleep for 2 second so that the current time changes
- SystemClock.sleep(2000);
- LocalDateTime timestampBeforeUpdate = LocalDateTime.now();
- JSONObject revocationList = mCertificateRevocationStatusManager.fetchRemoteRevocationList();
- List<String> otherCertificatesToCheck = new ArrayList<>();
- String serialNumber1 = "1234567"; // not revoked
- String serialNumber2 = "8350192447815228107"; // revoked
- String serialNumber3 = "987654"; // not revoked
- otherCertificatesToCheck.add(serialNumber1);
- otherCertificatesToCheck.add(serialNumber2);
- otherCertificatesToCheck.add(serialNumber3);
-
- mCertificateRevocationStatusManager
- .updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
- revocationList, otherCertificatesToCheck);
-
- Map<String, LocalDateTime> lastRevocationCheckData =
- mCertificateRevocationStatusManager.getLastRevocationCheckData();
- assertThat(lastRevocationCheckData.get(serialNumber1)).isAtLeast(timestampBeforeUpdate);
- assertThat(lastRevocationCheckData).doesNotContainKey(serialNumber2); // revoked
- assertThat(lastRevocationCheckData.get(serialNumber3)).isAtLeast(timestampBeforeUpdate);
- // validate that the existing certificates on the file got updated too
- for (X509Certificate certificate : mCertificates1) {
- assertThat(lastRevocationCheckData.get(getSerialNumber(certificate)))
- .isAtLeast(timestampBeforeUpdate);
- }
}
@Test
- public void checkRevocationStatus_allCertificatesRecentlyChecked_doesNotFetchRemoteCrl()
+ public void checkRevocationStatus_recentlyCheckedAndRevoked_exception()
throws Exception {
copyFromAssetToFile(
- REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
- mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
- // indirectly verifies the remote list is not fetched by simulating a remote revocation
- copyFromAssetToFile(
- REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
- // no exception
- mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ assertThrows(
+ CertPathValidatorException.class,
+ () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1));
}
@Test
- public void checkRevocationStatus_allCertificatesBarelyRecentlyChecked_doesNotFetchRemoteCrl()
+ public void checkRevocationStatus_barelyRecentlyChecked_doesNotFetchRemoteCrl()
throws Exception {
copyFromAssetToFile(
- REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
- Map<String, LocalDateTime> lastCheckedDates = new HashMap<>();
- LocalDateTime barelyRecently =
- LocalDateTime.now()
- .minusHours(
- CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK - 1);
- for (X509Certificate certificate : mCertificates1) {
- lastCheckedDates.put(getSerialNumber(certificate), barelyRecently);
- }
- mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // set the last modified date of the stored list to a barely recent date
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime barelyRecentDate =
+ now.minusHours(CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_FETCH - 1);
+ mStoredRevocationListFile.setLastModified(
+ barelyRecentDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000);
+ // indirectly verifies the remote list is not fetched by simulating a remote revocation
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
// Indirectly verify the remote CRL is not checked by checking there is no exception despite
- // a certificate being revoked. This test differs from the next only in the lastCheckedDate,
- // one before the NUM_HOURS_BEFORE_NEXT_CHECK cutoff and one after
+ // a certificate being revoked. This test differs from the next only in the stored list last
+ // modified date, one before the NUM_HOURS_BEFORE_NEXT_FETCH cutoff and one after
mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
}
@@ -322,21 +300,20 @@ public class CertificateRevocationStatusManagerTest {
public void checkRevocationStatus_certificatesRevokedAfterCheck_throwsException()
throws Exception {
copyFromAssetToFile(
- REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile);
+ REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
mCertificateRevocationStatusManager =
new CertificateRevocationStatusManager(
- mContext, mRevocationListUrl, mRevocationStatusFile, false);
- Map<String, LocalDateTime> lastCheckedDates = new HashMap<>();
- // To save network use, we do not check the remote CRL if all the certificates are recently
- // checked, so we set the lastCheckDate to some time not recent.
- LocalDateTime notRecently =
- LocalDateTime.now()
- .minusHours(
- CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK + 1);
- for (X509Certificate certificate : mCertificates1) {
- lastCheckedDates.put(getSerialNumber(certificate), notRecently);
- }
- mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates);
+ mContext, mRevocationListUrl, mStoredRevocationListFile, false);
+ mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1);
+ // set the last modified date of the stored list to a barely not recent date
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime barelyNotRecentDate =
+ now.minusHours(CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_FETCH + 1);
+ mStoredRevocationListFile.setLastModified(
+ barelyNotRecentDate.toEpochSecond(OffsetDateTime.now().getOffset()) * 1000);
+ // simulate a remote revocation
+ copyFromAssetToFile(
+ REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRemoteRevocationListFile);
assertThrows(
CertPathValidatorException.class,
@@ -362,8 +339,4 @@ public class CertificateRevocationStatusManagerTest {
fileOutputStream.write(data);
}
}
-
- private String getSerialNumber(X509Certificate certificate) {
- return certificate.getSerialNumber().toString(16);
- }
}