Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cf164810 authored by Tom Chan's avatar Tom Chan
Browse files

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
parent 3cf30217
Loading
Loading
Loading
Loading
+79 −202
Original line number Original line Diff line number Diff line
@@ -24,7 +24,7 @@ import android.content.Context;
import android.net.NetworkCapabilities;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.NetworkRequest;
import android.os.Environment;
import android.os.Environment;
import android.os.PersistableBundle;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.Slog;


import com.android.internal.R;
import com.android.internal.R;
@@ -35,6 +35,7 @@ import org.json.JSONObject;


import java.io.File;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStream;
@@ -42,52 +43,27 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URL;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertPathValidatorException;
import java.security.cert.X509Certificate;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.OffsetDateTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.List;
import java.util.Map;
import java.util.Set;


/** Manages the revocation status of certificates used in remote attestation. */
/** Manages the revocation status of certificates used in remote attestation. */
class CertificateRevocationStatusManager {
class CertificateRevocationStatusManager {
    private static final String TAG = "AVF_CRL";
    private static final String TAG = "AVF_CRL";
    // Must be unique within system server
    // Must be unique within system server
    private static final int JOB_ID = 1737671340;
    private static final int JOB_ID = 1737671340;
    private static final String REVOCATION_STATUS_FILE_NAME = "certificate_revocation_status.txt";
    private static final String REVOCATION_LIST_FILE_NAME = "certificate_revocation_list.json";
    private static final String REVOCATION_STATUS_FILE_FIELD_DELIMITER = ",";
    @VisibleForTesting static final int MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS = 30;


    /**
    @VisibleForTesting static final int NUM_HOURS_BEFORE_NEXT_FETCH = 24;
     * 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;


    private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
    private static final String TOP_LEVEL_JSON_PROPERTY_KEY = "entries";
    private static final Object sFileLock = new Object();
    private static final Object sFileLock = new Object();


    private final Context mContext;
    private final Context mContext;
    private final String mTestRemoteRevocationListUrl;
    private final String mTestRemoteRevocationListUrl;
    private final File mTestRevocationStatusFile;
    private final File mTestStoredRevocationListFile;
    private final boolean mShouldScheduleJob;
    private final boolean mShouldScheduleJob;


    CertificateRevocationStatusManager(Context context) {
    CertificateRevocationStatusManager(Context context) {
@@ -98,29 +74,22 @@ class CertificateRevocationStatusManager {
    CertificateRevocationStatusManager(
    CertificateRevocationStatusManager(
            Context context,
            Context context,
            String testRemoteRevocationListUrl,
            String testRemoteRevocationListUrl,
            File testRevocationStatusFile,
            File testStoredRevocationListFile,
            boolean shouldScheduleJob) {
            boolean shouldScheduleJob) {
        mContext = context;
        mContext = context;
        mTestRemoteRevocationListUrl = testRemoteRevocationListUrl;
        mTestRemoteRevocationListUrl = testRemoteRevocationListUrl;
        mTestRevocationStatusFile = testRevocationStatusFile;
        mTestStoredRevocationListFile = testStoredRevocationListFile;
        mShouldScheduleJob = shouldScheduleJob;
        mShouldScheduleJob = shouldScheduleJob;
    }
    }


    /**
    /**
     * Check the revocation status of the provided {@link X509Certificate}s.
     * 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
     * @param certificates List of certificates to be checked
     * @throws CertPathValidatorException if the check failed
     * @throws CertPathValidatorException if the check failed
     */
     */
    void checkRevocationStatus(List<X509Certificate> certificates)
    void checkRevocationStatus(List<X509Certificate> certificates)
            throws CertPathValidatorException {
            throws CertPathValidatorException {
        if (!needToCheckRevocationStatus(certificates)) {
            return;
        }
        List<String> serialNumbers = new ArrayList<>();
        List<String> serialNumbers = new ArrayList<>();
        for (X509Certificate certificate : certificates) {
        for (X509Certificate certificate : certificates) {
            String serialNumber = certificate.getSerialNumber().toString(16);
            String serialNumber = certificate.getSerialNumber().toString(16);
@@ -129,217 +98,124 @@ class CertificateRevocationStatusManager {
            }
            }
            serialNumbers.add(serialNumber);
            serialNumbers.add(serialNumber);
        }
        }
        LocalDateTime now = LocalDateTime.now();
        JSONObject revocationList;
        try {
        try {
            if (isLastCheckedWithin(Duration.ofHours(NUM_HOURS_BEFORE_NEXT_CHECK), serialNumbers)) {
            if (getLastModifiedDateTime(getRevocationListFile())
                Slog.d(
                    .isAfter(now.minusHours(NUM_HOURS_BEFORE_NEXT_FETCH))) {
                        TAG,
                Slog.d(TAG, "CRL is fetched recently, do not fetch again.");
                        "All certificates have been checked for revocation recently. No need to"
                revocationList = getStoredRevocationList();
                                + " check this time.");
                checkRevocationStatus(revocationList, serialNumbers);
                return;
                return;
            }
            }
        } catch (IOException ignored) {
        } catch (IOException | JSONException ignored) {
            // Proceed to check the revocation status
            // Proceed to fetch the remote revocation list
        }
        }
        try {
        try {
            JSONObject revocationList = fetchRemoteRevocationList();
            byte[] revocationListBytes = fetchRemoteRevocationListBytes();
            Map<String, Boolean> areCertificatesRevoked = new HashMap<>();
            silentlyStoreRevocationList(revocationListBytes);
            for (String serialNumber : serialNumbers) {
            revocationList = parseRevocationList(revocationListBytes);
                areCertificatesRevoked.put(serialNumber, revocationList.has(serialNumber));
            checkRevocationStatus(revocationList, serialNumbers);
            }
            updateLastRevocationCheckData(areCertificatesRevoked);
            for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
                if (entry.getValue()) {
                    throw new CertPathValidatorException(
                            "Certificate " + entry.getKey() + " has been revoked.");
                }
            }
        } catch (IOException | JSONException ex) {
        } catch (IOException | JSONException ex) {
            Slog.d(TAG, "Fallback to check stored revocation status", ex);
            Slog.d(TAG, "Fallback to check stored revocation status", ex);
            if (ex instanceof IOException && mShouldScheduleJob) {
            if (ex instanceof IOException && mShouldScheduleJob) {
                scheduleJobToUpdateStoredDataWithRemoteRevocationList(serialNumbers);
                scheduleJobToFetchRemoteRevocationJob();
            }
            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);
                }
            }
            }
            try {
            try {
                if (!isLastCheckedWithin(
                revocationList = getStoredRevocationList();
                        Duration.ofDays(MAX_DAYS_SINCE_LAST_CHECK), serialNumbers)) {
                checkRevocationStatus(revocationList, serialNumbers);
            } catch (IOException | JSONException ex2) {
                throw new CertPathValidatorException(
                throw new CertPathValidatorException(
                            "Unable to verify the revocation status of one of the certificates "
                        "Unable to load or parse stored revocation status", ex2);
                                    + serialNumbers);
                }
            } catch (IOException ex2) {
                throw new CertPathValidatorException(
                        "Unable to load stored revocation status", ex2);
            }
            }
        }
        }
    }
    }


    private boolean isLastCheckedWithin(Duration lastCheckedWithin, List<String> serialNumbers)
    private static void checkRevocationStatus(JSONObject revocationList, List<String> serialNumbers)
            throws IOException {
            throws CertPathValidatorException {
        Map<String, LocalDateTime> lastRevocationCheckData = getLastRevocationCheckData();
        for (String serialNumber : serialNumbers) {
        for (String serialNumber : serialNumbers) {
            if (!lastRevocationCheckData.containsKey(serialNumber)
            if (revocationList.has(serialNumber)) {
                    || lastRevocationCheckData
                throw new CertPathValidatorException(
                            .get(serialNumber)
                        "Certificate has been revoked: " + serialNumber);
                            .isBefore(LocalDateTime.now().minus(lastCheckedWithin))) {
                return false;
            }
            }
        }
        }
        return true;
    }
    }


    private static boolean needToCheckRevocationStatus(
    private JSONObject getStoredRevocationList() throws IOException, JSONException {
            List<X509Certificate> certificatesOrderedLeafFirst) {
        File offlineRevocationListFile = getRevocationListFile();
        if (certificatesOrderedLeafFirst.isEmpty()) {
        if (!offlineRevocationListFile.exists()
            return false;
                || isRevocationListExpired(offlineRevocationListFile)) {
            throw new FileNotFoundException("Offline copy does not exist or has expired.");
        }
        }
        // A certificate isn't revoked when it is first issued, so we treat it as checked on its
        synchronized (sFileLock) {
        // issue date.
            try (FileInputStream inputStream = new FileInputStream(offlineRevocationListFile)) {
        if (!isIssuedWithinDays(certificatesOrderedLeafFirst.get(0), MAX_DAYS_SINCE_LAST_CHECK)) {
                return parseRevocationList(inputStream.readAllBytes());
            return true;
        }
        for (int i = 1; i < certificatesOrderedLeafFirst.size(); i++) {
            if (!isIssuedWithinDays(
                    certificatesOrderedLeafFirst.get(i), FRESH_INTERMEDIARY_CERT_DAYS)) {
                return true;
            }
            }
        }
        }
        return false;
    }
    }


    private static boolean isIssuedWithinDays(X509Certificate certificate, int days) {
    private boolean isRevocationListExpired(File offlineRevocationListFile) {
        LocalDate notBeforeDate =
        LocalDateTime acceptableLastModifiedDate =
                LocalDate.ofInstant(certificate.getNotBefore().toInstant(), ZoneId.systemDefault());
                LocalDateTime.now().minusDays(MAX_OFFLINE_REVOCATION_LIST_AGE_DAYS);
        LocalDate expectedIssueData =
        LocalDateTime lastModifiedDate = getLastModifiedDateTime(offlineRevocationListFile);
                notBeforeDate.plusDays(DAYS_BETWEEN_ISSUE_AND_NOT_BEFORE_DATES);
        return lastModifiedDate.isBefore(acceptableLastModifiedDate);
        return LocalDate.now().minusDays(days + 1).isBefore(expectedIssueData);
    }
    }


    void updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
    private static LocalDateTime getLastModifiedDateTime(File file) {
            JSONObject revocationList, Collection<String> otherCertificatesToCheck) {
        // if the file does not exist, file.lastModified() returns 0, so this method returns the
        Set<String> allCertificatesToCheck = new HashSet<>(otherCertificatesToCheck);
        // epoch time
        try {
        return LocalDateTime.ofEpochSecond(
            allCertificatesToCheck.addAll(getLastRevocationCheckData().keySet());
                file.lastModified() / 1000, 0, OffsetDateTime.now().getOffset());
        } 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);
    }
    }


    /**
    /**
     * 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
     * <p>This method internally synchronize file access with other methods in this class.
     *     whether that certificate has been revoked
     *
     * @param revocationListBytes The bytes to store to the local revocation list file.
     */
     */
    void updateLastRevocationCheckData(Map<String, Boolean> areCertificatesRevoked) {
    void silentlyStoreRevocationList(byte[] revocationListBytes) {
        LocalDateTime now = LocalDateTime.now();
        synchronized (sFileLock) {
        synchronized (sFileLock) {
            Map<String, LocalDateTime> lastRevocationCheckData;
            AtomicFile atomicRevocationListFile = new AtomicFile(getRevocationListFile());
            FileOutputStream fileOutputStream = null;
            try {
            try {
                lastRevocationCheckData = getLastRevocationCheckData();
                fileOutputStream = atomicRevocationListFile.startWrite();
                fileOutputStream.write(revocationListBytes);
                atomicRevocationListFile.finishWrite(fileOutputStream);
                Slog.d(TAG, "Successfully stored revocation list.");
            } catch (IOException ex) {
            } catch (IOException ex) {
                Slog.e(TAG, "Unable to updateLastRevocationCheckData", ex);
                Slog.e(TAG, "Failed to store the certificate revocation list.", ex);
                return;
                // this happens when fileOutputStream.write fails
            }
                if (fileOutputStream != null) {
            for (Map.Entry<String, Boolean> entry : areCertificatesRevoked.entrySet()) {
                    atomicRevocationListFile.failWrite(fileOutputStream);
                if (entry.getValue()) {
                    lastRevocationCheckData.remove(entry.getKey());
                } else {
                    lastRevocationCheckData.put(entry.getKey(), now);
                }
            }
            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;
                }
            }
        }
        return data;
    }

    @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() {
    private File getRevocationListFile() {
        if (mTestRevocationStatusFile != null) {
        if (mTestStoredRevocationListFile != null) {
            return mTestRevocationStatusFile;
            return mTestStoredRevocationListFile;
        }
        }
        return new File(Environment.getDataSystemDirectory(), REVOCATION_STATUS_FILE_NAME);
        return new File(Environment.getDataSystemDirectory(), REVOCATION_LIST_FILE_NAME);
    }
    }


    private void scheduleJobToUpdateStoredDataWithRemoteRevocationList(List<String> serialNumbers) {
    private void scheduleJobToFetchRemoteRevocationJob() {
        JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
        JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class);
        if (jobScheduler == null) {
        if (jobScheduler == null) {
            Slog.e(TAG, "Unable to get job scheduler.");
            Slog.e(TAG, "Unable to get job scheduler.");
            return;
            return;
        }
        }
        Slog.d(TAG, "Scheduling job to fetch remote CRL.");
        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(
        jobScheduler.schedule(
                new JobInfo.Builder(
                new JobInfo.Builder(
                                JOB_ID,
                                JOB_ID,
                                new ComponentName(
                                new ComponentName(
                                        mContext,
                                        mContext,
                                        UpdateCertificateRevocationStatusJobService.class))
                                        UpdateCertificateRevocationStatusJobService.class))
                        .setExtras(extras)
                        .setRequiredNetwork(
                        .setRequiredNetwork(
                                new NetworkRequest.Builder()
                                new NetworkRequest.Builder()
                                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
@@ -351,13 +227,11 @@ class CertificateRevocationStatusManager {
     * Fetches the revocation list from the URL specified in
     * Fetches the revocation list from the URL specified in
     * R.string.vendor_required_attestation_revocation_list_url
     * 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 CertPathValidatorException if the URL is not defined or is malformed.
     * @throws IOException if the URL is valid but the fetch failed.
     * @throws IOException if the URL is valid but the fetch failed.
     * @throws JSONException if the revocation list content cannot be parsed
     */
     */
    JSONObject fetchRemoteRevocationList()
    byte[] fetchRemoteRevocationListBytes() throws CertPathValidatorException, IOException {
            throws CertPathValidatorException, IOException, JSONException {
        String urlString = getRemoteRevocationListUrl();
        String urlString = getRemoteRevocationListUrl();
        if (urlString == null || urlString.isEmpty()) {
        if (urlString == null || urlString.isEmpty()) {
            throw new CertPathValidatorException(
            throw new CertPathValidatorException(
@@ -369,10 +243,13 @@ class CertificateRevocationStatusManager {
        } catch (MalformedURLException ex) {
        } catch (MalformedURLException ex) {
            throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex);
            throw new CertPathValidatorException("Unable to parse the URL " + urlString, ex);
        }
        }
        byte[] revocationListBytes;
        try (InputStream inputStream = url.openStream()) {
        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));
        JSONObject revocationListJson = new JSONObject(new String(revocationListBytes, UTF_8));
        return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
        return revocationListJson.getJSONObject(TOP_LEVEL_JSON_PROPERTY_KEY);
    }
    }
+9 −19
Original line number Original line Diff line number Diff line
@@ -20,17 +20,15 @@ import android.app.job.JobParameters;
import android.app.job.JobService;
import android.app.job.JobService;
import android.util.Slog;
import android.util.Slog;


import org.json.JSONObject;

import java.util.Arrays;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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 {
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 static final String TAG = "AVF_CRL";
    private ExecutorService mExecutorService;
    private ExecutorService mExecutorService;


@@ -48,20 +46,12 @@ public class UpdateCertificateRevocationStatusJobService extends JobService {
                        CertificateRevocationStatusManager certificateRevocationStatusManager =
                        CertificateRevocationStatusManager certificateRevocationStatusManager =
                                new CertificateRevocationStatusManager(this);
                                new CertificateRevocationStatusManager(this);
                        Slog.d(TAG, "Starting to fetch remote CRL from job service.");
                        Slog.d(TAG, "Starting to fetch remote CRL from job service.");
                        JSONObject revocationList =
                        byte[] revocationList =
                                certificateRevocationStatusManager.fetchRemoteRevocationList();
                                certificateRevocationStatusManager.fetchRemoteRevocationListBytes();
                        String[] certificatesToCheckFromJobParams =
                        certificateRevocationStatusManager.silentlyStoreRevocationList(
                                params.getExtras().getStringArray(EXTRA_KEY_CERTIFICATES_TO_CHECK);
                                revocationList);
                        if (certificatesToCheckFromJobParams == null) {
                            Slog.e(TAG, "Extras not found: " + EXTRA_KEY_CERTIFICATES_TO_CHECK);
                            return;
                        }
                        certificateRevocationStatusManager
                                .updateLastRevocationCheckDataForAllPreviouslySeenCertificates(
                                        revocationList,
                                        Arrays.asList(certificatesToCheckFromJobParams));
                    } catch (Throwable t) {
                    } 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);
                    jobFinished(params, false);
                });
                });
+120 −147

File changed.

Preview size limit exceeded, changes collapsed.