Add new metrics to rkpd for Android U

1. Add certificate chain length and root info to ProvisioningAttempt
2. Add RkpdClientOperation metric to track the status of every single
   client call into rkpd
3. Periodically report the pool stats so we can get a picture of the
   health of the key pools of devices in the field

Bug: 266509149
Bug: 268247931
Test: RkpdAppIntegrationTests RkpdAppUnitTests RkpdAppStressTests
Change-Id: I1c277271429478bcc6b989e1ac676be1645ce8a2
diff --git a/app/src/com/android/rkpdapp/interfaces/ServerInterface.java b/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
index f357557..b36be88 100644
--- a/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
+++ b/app/src/com/android/rkpdapp/interfaces/ServerInterface.java
@@ -29,6 +29,7 @@
 import com.android.rkpdapp.utils.CborUtils;
 import com.android.rkpdapp.utils.Settings;
 import com.android.rkpdapp.utils.StopWatch;
+import com.android.rkpdapp.utils.X509Utils;
 
 import java.io.BufferedInputStream;
 import java.io.ByteArrayOutputStream;
@@ -40,6 +41,9 @@
 import java.net.URL;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.X509Certificate;
 import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
@@ -122,6 +126,20 @@
             throw new RkpdException(
                     RkpdException.ErrorCode.INTERNAL_ERROR,
                     "Response failed to parse.");
+        } else if (certChains.isEmpty()) {
+            metrics.setCertChainLength(0);
+            metrics.setRootCertFingerprint("");
+        } else {
+            try {
+                X509Certificate[] certs = X509Utils.formatX509Certs(certChains.get(0));
+                metrics.setCertChainLength(certs.length);
+                byte[] pubKey = certs[certs.length - 1].getPublicKey().getEncoded();
+                byte[] pubKeyDigest = MessageDigest.getInstance("SHA-256").digest(pubKey);
+                metrics.setRootCertFingerprint(Base64.encodeToString(pubKeyDigest, Base64.DEFAULT));
+            } catch (NoSuchAlgorithmException e) {
+                throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
+                        "Algorithm not found", e);
+            }
         }
         return certChains;
     }
diff --git a/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java b/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
index 3dcf98e..4d24219 100644
--- a/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
+++ b/app/src/com/android/rkpdapp/metrics/ProvisioningAttempt.java
@@ -77,7 +77,9 @@
     private Enablement mEnablement;
     private boolean mIsKeyPoolEmpty = false;
     private Status mStatus = Status.UNKNOWN;
-    private int mHttpStatusError = 0;
+    private int mHttpStatusError;
+    private String mRootCertFingerprint = "<none>";
+    private int mCertChainLength;
 
     private ProvisioningAttempt(Context context, int cause,
             String remotelyProvisionedComponent, Enablement enablement) {
@@ -141,6 +143,14 @@
         mHttpStatusError = httpStatusError;
     }
 
+    public void setRootCertFingerprint(String rootCertFingerprint) {
+        mRootCertFingerprint = rootCertFingerprint;
+    }
+
+    public void setCertChainLength(int certChainLength) {
+        mCertChainLength = certChainLength;
+    }
+
     /**
      * Starts the server wait timer, returning a reference to an object to be closed when the
      * wait is over.
@@ -176,7 +186,7 @@
         int transportType = getTransportTypeForActiveNetwork();
         RkpdStatsLog.write(RkpdStatsLog.REMOTE_KEY_PROVISIONING_ATTEMPT,
                 mCause, mRemotelyProvisionedComponent, getUpTimeBucket(), getIntEnablement(),
-                mIsKeyPoolEmpty, getIntStatus(), "", 0);
+                mIsKeyPoolEmpty, getIntStatus(), mRootCertFingerprint, mCertChainLength);
         RkpdStatsLog.write(
                 RkpdStatsLog.REMOTE_KEY_PROVISIONING_NETWORK_INFO,
                 transportType, getIntStatus(), mHttpStatusError);
diff --git a/app/src/com/android/rkpdapp/metrics/RkpdClientOperation.java b/app/src/com/android/rkpdapp/metrics/RkpdClientOperation.java
new file mode 100644
index 0000000..f4e23ff
--- /dev/null
+++ b/app/src/com/android/rkpdapp/metrics/RkpdClientOperation.java
@@ -0,0 +1,110 @@
+/**
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.rkpdapp.metrics;
+
+import com.android.rkpdapp.service.RemoteProvisioningService;
+import com.android.rkpdapp.utils.StopWatch;
+
+/**
+ * Contains the metrics values that are recorded for every client call into RKPD.
+ * This class will automatically push an atom on close, and is intended to be used with a
+ * try-with-resources block to ensure metrics are automatically logged.
+ */
+public final class RkpdClientOperation implements AutoCloseable {
+    private static final String TAG = RemoteProvisioningService.TAG;
+
+    public enum Result {
+        UNKNOWN(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_UNKNOWN),
+        SUCCESS(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_SUCCESS),
+        CANCELED(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_CANCELED),
+        RKP_UNSUPPORTED(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_RKP_UNSUPPORTED),
+        ERROR_INTERNAL(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_ERROR_INTERNAL),
+        ERROR_REQUIRES_SECURITY_PATCH(
+                RkpdStatsLog
+                        .RKPD_CLIENT_OPERATION__RESULT__RESULT_ERROR_REQUIRES_SECURITY_PATCH),
+        ERROR_PENDING_INTERNET_CONNECTIVITY(
+                RkpdStatsLog
+                        .RKPD_CLIENT_OPERATION__RESULT__RESULT_ERROR_PENDING_INTERNET_CONNECTIVITY),
+        ERROR_PERMANENT(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_ERROR_PERMANENT),
+        ERROR_INVALID_HAL(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_ERROR_INVALID_HAL),
+        ERROR_KEY_NOT_FOUND(RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_ERROR_KEY_NOT_FOUND);
+
+        private final int mAtomValue;
+
+        Result(int atomValue) {
+            mAtomValue = atomValue;
+        }
+
+        public int getAtomValue() {
+            return mAtomValue;
+        }
+    }
+
+    private final StopWatch mTimer = new StopWatch(TAG);
+    private final int mClientUid;
+    private final String mRemotelyProvisionedComponent;
+    private final int mOperationId;
+    private int mResult = RkpdStatsLog.RKPD_CLIENT_OPERATION__RESULT__RESULT_UNKNOWN;
+
+    /** Create an object that records an atom for a getRegistration call */
+    public static RkpdClientOperation getRegistration(int clientUid,
+            String remotelyProvisionedComponent) {
+        return new RkpdClientOperation(clientUid, remotelyProvisionedComponent,
+                RkpdStatsLog.RKPD_CLIENT_OPERATION__OPERATION__OPERATION_GET_REGISTRATION);
+    }
+
+    /** Create an object that records an atom for a getKey call */
+    public static RkpdClientOperation getKey(int clientUid,
+            String remotelyProvisionedComponent) {
+        return new RkpdClientOperation(clientUid, remotelyProvisionedComponent,
+                RkpdStatsLog.RKPD_CLIENT_OPERATION__OPERATION__OPERATION_GET_KEY);
+    }
+
+    /** Create an object that records an atom for a cancelGetKey call */
+    public static RkpdClientOperation cancelGetKey(int clientUid,
+            String remotelyProvisionedComponent) {
+        return new RkpdClientOperation(clientUid, remotelyProvisionedComponent,
+                RkpdStatsLog.RKPD_CLIENT_OPERATION__OPERATION__OPERATION_CANCEL_GET_KEY);
+    }
+
+    /** Create an object that records an atom for a storeUpgradedKey call */
+    public static RkpdClientOperation storeUpgradedKey(int clientUid,
+            String remotelyProvisionedComponent) {
+        return new RkpdClientOperation(clientUid, remotelyProvisionedComponent,
+                RkpdStatsLog.RKPD_CLIENT_OPERATION__OPERATION__OPERATION_STORE_UPGRADED_KEY);
+    }
+
+    private RkpdClientOperation(int clientUid, String remotelyProvisionedComponent,
+            int operationId) {
+        mClientUid = clientUid;
+        mRemotelyProvisionedComponent = remotelyProvisionedComponent;
+        mOperationId = operationId;
+        mTimer.start();
+    }
+
+    public void setResult(Result result) {
+        mResult = result.getAtomValue();
+    }
+
+    /** Record the atoms for this metrics object. */
+    @Override
+    public void close() {
+        mTimer.stop();
+        RkpdStatsLog.write(RkpdStatsLog.RKPD_CLIENT_OPERATION, mRemotelyProvisionedComponent,
+                mClientUid, mOperationId, mResult, mTimer.getElapsedMillis());
+    }
+}
diff --git a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
index e7f7804..c968b3a 100644
--- a/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
+++ b/app/src/com/android/rkpdapp/provisioner/PeriodicProvisioner.java
@@ -32,6 +32,7 @@
 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
 import com.android.rkpdapp.interfaces.SystemInterface;
 import com.android.rkpdapp.metrics.ProvisioningAttempt;
+import com.android.rkpdapp.metrics.RkpdStatsLog;
 import com.android.rkpdapp.utils.Settings;
 
 import java.time.Instant;
@@ -107,6 +108,7 @@
                 Log.i(TAG, "Starting provisioning for " + irpc);
                 try {
                     provisioner.provisionKeys(metrics, irpc, response);
+                    recordKeyPoolStatsAtom(irpc);
                     Log.i(TAG, "Successfully provisioned " + irpc);
                 } catch (CborException e) {
                     Log.e(TAG, "Error parsing CBOR for " + irpc, e);
@@ -119,4 +121,16 @@
             return result;
         }
     }
+
+    private void recordKeyPoolStatsAtom(SystemInterface irpc) {
+        String halName = irpc.getServiceName();
+        final int numExpiring = mKeyDao.getTotalExpiringKeysForIrpc(halName,
+                Settings.getExpirationTime(mContext));
+        final int numUnassigned = mKeyDao.getTotalUnassignedKeysForIrpc(halName);
+        final int total = mKeyDao.getTotalKeysForIrpc(halName);
+        Log.i(TAG, "Logging atom metric for pool status, total: " + total + ", numExpiring: "
+                + numExpiring + ", numUnassigned: " + numUnassigned);
+        RkpdStatsLog.write(RkpdStatsLog.RKPD_POOL_STATS, irpc.getServiceName(), numExpiring,
+                numUnassigned, total);
+    }
 }
diff --git a/app/src/com/android/rkpdapp/provisioner/Provisioner.java b/app/src/com/android/rkpdapp/provisioner/Provisioner.java
index 3e90d21..e42b11b 100644
--- a/app/src/com/android/rkpdapp/provisioner/Provisioner.java
+++ b/app/src/com/android/rkpdapp/provisioner/Provisioner.java
@@ -17,7 +17,6 @@
 package com.android.rkpdapp.provisioner;
 
 import android.content.Context;
-import android.util.Base64;
 import android.util.Log;
 
 import com.android.rkpdapp.GeekResponse;
@@ -33,10 +32,6 @@
 import com.android.rkpdapp.utils.StatsProcessor;
 import com.android.rkpdapp.utils.X509Utils;
 
-import java.security.InvalidAlgorithmParameterException;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.util.ArrayList;
@@ -157,16 +152,7 @@
             List<RkpKey> keysGenerated) throws RkpdException {
         List<ProvisionedKey> provisionedKeys = new ArrayList<>();
         for (byte[] chain : certChains) {
-            X509Certificate cert;
-            try {
-                cert = X509Utils.formatX509Certs(chain)[0];
-            } catch (CertificateException | NoSuchAlgorithmException | NoSuchProviderException
-                    | InvalidAlgorithmParameterException e) {
-                Log.e(TAG, "Unable to parse certificate chain."
-                        + Base64.encodeToString(chain, Base64.DEFAULT), e);
-                throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
-                        "Failed to interpret DER encoded certificate chain", e);
-            }
+            X509Certificate cert = X509Utils.formatX509Certs(chain)[0];
             long expirationDate = cert.getNotAfter().getTime();
             byte[] rawPublicKey = X509Utils.getAndFormatRawPublicKey(cert);
             if (rawPublicKey == null) {
diff --git a/app/src/com/android/rkpdapp/service/RegistrationBinder.java b/app/src/com/android/rkpdapp/service/RegistrationBinder.java
index 3462981..9402d63 100644
--- a/app/src/com/android/rkpdapp/service/RegistrationBinder.java
+++ b/app/src/com/android/rkpdapp/service/RegistrationBinder.java
@@ -33,6 +33,7 @@
 import com.android.rkpdapp.interfaces.ServerInterface;
 import com.android.rkpdapp.interfaces.SystemInterface;
 import com.android.rkpdapp.metrics.ProvisioningAttempt;
+import com.android.rkpdapp.metrics.RkpdClientOperation;
 import com.android.rkpdapp.provisioner.Provisioner;
 import com.android.rkpdapp.utils.Settings;
 
@@ -216,45 +217,63 @@
                         + " is already associated with a getKey operation that is in-progress");
             }
 
-            mTasks.put(callback, mThreadPool.submit(() -> {
-                try {
-                    getKeyWorker(keyId, callback);
-                } catch (InterruptedException e) {
-                    Log.i(TAG, "getKey was interrupted");
-                    checkedCallback(callback::onCancel);
-                } catch (RkpdException e) {
-                    Log.e(TAG, "RKPD failed to provision keys", e);
-                    checkedCallback(() -> callback.onError(mapToGetKeyError(e), e.getMessage()));
-                } catch (Exception e) {
-                    // Do our best to inform the callback when the unexpected happens. Otherwise,
-                    // the caller is going to wait until they timeout without knowing something like
-                    // a RuntimeException occurred.
-                    Log.e(TAG, "Unexpected error provisioning keys", e);
-                    checkedCallback(() -> callback.onError(IGetKeyCallback.Error.ERROR_UNKNOWN,
-                            e.getMessage()));
-                } finally {
-                    synchronized (mTasksLock) {
-                        mTasks.remove(callback);
-                    }
-                }
-            }));
+            mTasks.put(callback, mThreadPool.submit(() -> getKeyThreadWorker(keyId, callback)));
+        }
+    }
+
+    private void getKeyThreadWorker(int keyId, IGetKeyCallback callback) {
+        // We don't use a try-with-resources here because the metric may need to be updated
+        // inside an exception handler, but close would have been called prior to that. Therefore,
+        // we explicitly close the metric explicitly in the "finally" block, after all handlers
+        // have had a chance to run.
+        RkpdClientOperation metric = RkpdClientOperation.getKey(mClientUid,
+                mSystemInterface.getServiceName());
+        try {
+            getKeyWorker(keyId, callback);
+            metric.setResult(RkpdClientOperation.Result.SUCCESS);
+        } catch (InterruptedException e) {
+            Log.i(TAG, "getKey was interrupted");
+            metric.setResult(RkpdClientOperation.Result.CANCELED);
+            checkedCallback(callback::onCancel);
+        } catch (RkpdException e) {
+            Log.e(TAG, "RKPD failed to provision keys", e);
+            final byte mappedError = mapToGetKeyError(e, metric);
+            checkedCallback(
+                    () -> callback.onError(mappedError, e.getMessage()));
+        } catch (Exception e) {
+            // Do our best to inform the callback when the unexpected happens. Otherwise,
+            // the caller is going to wait until they timeout without knowing something like
+            // a RuntimeException occurred.
+            Log.e(TAG, "Unexpected error provisioning keys", e);
+            checkedCallback(() -> callback.onError(IGetKeyCallback.Error.ERROR_UNKNOWN,
+                    e.getMessage()));
+        } finally {
+            metric.close();
+            synchronized (mTasksLock) {
+                mTasks.remove(callback);
+            }
         }
     }
 
     /** Maps an RkpdException into an IGetKeyCallback.Error value. */
-    private byte mapToGetKeyError(RkpdException e) {
+    private byte mapToGetKeyError(RkpdException e, RkpdClientOperation metric) {
         switch (e.getErrorCode()) {
             case NO_NETWORK_CONNECTIVITY:
+                metric.setResult(RkpdClientOperation.Result.ERROR_PENDING_INTERNET_CONNECTIVITY);
                 return IGetKeyCallback.Error.ERROR_PENDING_INTERNET_CONNECTIVITY;
 
             case DEVICE_NOT_REGISTERED:
+                metric.setResult(RkpdClientOperation.Result.ERROR_PERMANENT);
                 return IGetKeyCallback.Error.ERROR_PERMANENT;
 
+            case INTERNAL_ERROR:
+                metric.setResult(RkpdClientOperation.Result.ERROR_INTERNAL);
+                return IGetKeyCallback.Error.ERROR_UNKNOWN;
+
             case NETWORK_COMMUNICATION_ERROR:
             case HTTP_CLIENT_ERROR:
             case HTTP_SERVER_ERROR:
             case HTTP_UNKNOWN_ERROR:
-            case INTERNAL_ERROR:
             default:
                 return IGetKeyCallback.Error.ERROR_UNKNOWN;
         }
@@ -264,16 +283,20 @@
     public void cancelGetKey(IGetKeyCallback callback) throws RemoteException {
         Log.i(TAG, "cancelGetKey(" + callback.hashCode() + ")");
         synchronized (mTasksLock) {
-            Future<?> task = mTasks.get(callback);
+            try (RkpdClientOperation metric = RkpdClientOperation.cancelGetKey(mClientUid,
+                    mSystemInterface.getServiceName())) {
+                Future<?> task = mTasks.get(callback);
 
-            if (task == null) {
-                Log.w(TAG, "callback not found, task may have already completed");
-            } else if (task.isDone()) {
-                Log.w(TAG, "task already completed, not cancelling");
-            } else if (task.isCancelled()) {
-                Log.w(TAG, "task already cancelled, cannot cancel it any further");
-            } else {
-                task.cancel(true);
+                if (task == null) {
+                    Log.w(TAG, "callback not found, task may have already completed");
+                } else if (task.isDone()) {
+                    Log.w(TAG, "task already completed, not cancelling");
+                } else if (task.isCancelled()) {
+                    Log.w(TAG, "task already cancelled, cannot cancel it any further");
+                } else {
+                    task.cancel(true);
+                }
+                metric.setResult(RkpdClientOperation.Result.SUCCESS);
             }
         }
     }
@@ -283,14 +306,18 @@
             IStoreUpgradedKeyCallback callback) throws RemoteException {
         Log.i(TAG, "storeUpgradedKeyAsync");
         mThreadPool.execute(() -> {
-            try {
+            try (RkpdClientOperation metric = RkpdClientOperation.storeUpgradedKey(
+                    mClientUid, mSystemInterface.getServiceName())) {
                 int keysUpgraded = mProvisionedKeyDao.upgradeKeyBlob(mClientUid, oldKeyBlob,
                         newKeyBlob);
                 if (keysUpgraded == 1) {
+                    metric.setResult(RkpdClientOperation.Result.SUCCESS);
                     checkedCallback(callback::onSuccess);
                 } else if (keysUpgraded == 0) {
+                    metric.setResult(RkpdClientOperation.Result.ERROR_KEY_NOT_FOUND);
                     checkedCallback(() -> callback.onError("No keys matching oldKeyBlob found"));
                 } else {
+                    metric.setResult(RkpdClientOperation.Result.ERROR_INTERNAL);
                     Log.e(TAG, "Multiple keys matched the upgrade (" + keysUpgraded
                             + "). This should be impossible!");
                     checkedCallback(() -> callback.onError("Internal error"));
diff --git a/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java b/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
index e8f01b8..544d59d 100644
--- a/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
+++ b/app/src/com/android/rkpdapp/service/RemoteProvisioningService.java
@@ -32,6 +32,7 @@
 import com.android.rkpdapp.interfaces.ServerInterface;
 import com.android.rkpdapp.interfaces.ServiceManagerInterface;
 import com.android.rkpdapp.interfaces.SystemInterface;
+import com.android.rkpdapp.metrics.RkpdClientOperation;
 import com.android.rkpdapp.provisioner.Provisioner;
 import com.android.rkpdapp.utils.Settings;
 
@@ -54,10 +55,12 @@
         @Override
         public void getRegistration(int callerUid, String irpcName,
                 IGetRegistrationCallback callback) {
-            try {
-                final Context context = getApplicationContext();
+            final Context context = getApplicationContext();
+            RkpdClientOperation metric = RkpdClientOperation.getRegistration(callerUid, irpcName);
+            try (metric) {
                 if (Settings.getDefaultUrl().isEmpty()) {
                     callback.onError("RKP is disabled. System configured with no default URL.");
+                    metric.setResult(RkpdClientOperation.Result.RKP_UNSUPPORTED);
                     return;
                 }
 
@@ -67,6 +70,7 @@
                 } catch (IllegalArgumentException e) {
                     Log.e(TAG, "Error getting HAL '" + irpcName + "'", e);
                     callback.onError("Invalid HAL name: " + irpcName);
+                    metric.setResult(RkpdClientOperation.Result.ERROR_INVALID_HAL);
                     return;
                 }
 
@@ -75,9 +79,11 @@
                 IRegistration.Stub registration = new RegistrationBinder(context, callerUid,
                         systemInterface, dao, new ServerInterface(context), provisioner,
                         ThreadPool.EXECUTOR);
+                metric.setResult(RkpdClientOperation.Result.SUCCESS);
                 callback.onSuccess(registration);
             } catch (RemoteException e) {
                 Log.e(TAG, "Error notifying callback binder", e);
+                metric.setResult(RkpdClientOperation.Result.ERROR_INTERNAL);
                 throw e.rethrowAsRuntimeException();
             }
         }
diff --git a/app/src/com/android/rkpdapp/utils/X509Utils.java b/app/src/com/android/rkpdapp/utils/X509Utils.java
index e48cdd2..4e12872 100644
--- a/app/src/com/android/rkpdapp/utils/X509Utils.java
+++ b/app/src/com/android/rkpdapp/utils/X509Utils.java
@@ -16,8 +16,11 @@
 
 package com.android.rkpdapp.utils;
 
+import android.util.Base64;
 import android.util.Log;
 
+import com.android.rkpdapp.RkpdException;
+
 import java.io.ByteArrayInputStream;
 import java.math.BigInteger;
 import java.security.InvalidAlgorithmParameterException;
@@ -50,17 +53,24 @@
      * Takes a byte array composed of DER encoded certificates and returns the X.509 certificates
      * contained within as an X509Certificate array.
      */
-    public static X509Certificate[] formatX509Certs(byte[] certStream)
-            throws CertificateException, InvalidAlgorithmParameterException,
-            NoSuchAlgorithmException, NoSuchProviderException {
-        CertificateFactory fact = CertificateFactory.getInstance("X.509");
-        ByteArrayInputStream in = new ByteArrayInputStream(certStream);
-        ArrayList<Certificate> certs = new ArrayList<>(fact.generateCertificates(in));
-        X509Certificate[] certChain = certs.toArray(new X509Certificate[0]);
-        if (isCertChainValid(certChain)) {
-            return certChain;
-        } else {
-            throw new CertificateException("Could not validate certificate chain.");
+    public static X509Certificate[] formatX509Certs(byte[] certStream) throws RkpdException {
+        try {
+            CertificateFactory fact = CertificateFactory.getInstance("X.509");
+            ByteArrayInputStream in = new ByteArrayInputStream(certStream);
+            ArrayList<Certificate> certs = new ArrayList<>(fact.generateCertificates(in));
+            X509Certificate[] certChain = certs.toArray(new X509Certificate[0]);
+            if (isCertChainValid(certChain)) {
+                return certChain;
+            } else {
+                throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
+                        "Could not validate certificate chain.");
+            }
+        } catch (CertificateException | NoSuchAlgorithmException | NoSuchProviderException
+                 | InvalidAlgorithmParameterException e) {
+            Log.e(TAG, "Unable to parse certificate chain."
+                    + Base64.encodeToString(certStream, Base64.DEFAULT), e);
+            throw new RkpdException(RkpdException.ErrorCode.INTERNAL_ERROR,
+                    "Failed to interpret DER encoded certificate chain", e);
         }
     }