diff options
author | 2025-02-19 21:13:48 +0000 | |
---|---|---|
committer | 2025-02-21 19:42:40 +0000 | |
commit | cf1648107aab44b4fdcee1a2a96ec9abb6e0c2a7 (patch) | |
tree | 3de45f9e24f3eebb107896a83ca9f00096b12bdc | |
parent | 3cf302173647ce5c5269358225c2f496bdb34c50 (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
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); - } } |