Merge "Revert "Add debug logs""
diff --git a/cmds/statsd/src/metrics/MetricsManager.cpp b/cmds/statsd/src/metrics/MetricsManager.cpp
index 417145c..5f0619a 100644
--- a/cmds/statsd/src/metrics/MetricsManager.cpp
+++ b/cmds/statsd/src/metrics/MetricsManager.cpp
@@ -28,6 +28,7 @@
 #include "stats_util.h"
 
 #include <log/logprint.h>
+#include <private/android_filesystem_config.h>
 
 using android::util::FIELD_COUNT_REPEATED;
 using android::util::FIELD_TYPE_MESSAGE;
@@ -47,7 +48,7 @@
 
 MetricsManager::MetricsManager(const ConfigKey& key, const StatsdConfig& config,
                                const long timeBaseSec, sp<UidMap> uidMap)
-    : mConfigKey(key), mUidMap(uidMap), mStatsdUid(getStatsdUid()) {
+    : mConfigKey(key), mUidMap(uidMap) {
     mConfigValid =
             initStatsdConfig(key, config, *uidMap, timeBaseSec, mTagIds, mAllAtomMatchers, mAllConditionTrackers,
                              mAllMetricProducers, mAllAnomalyTrackers, mConditionToMetricMap,
@@ -59,9 +60,9 @@
         // mConfigValid = false;
         // ALOGE("Log source white list is empty! This config won't get any data.");
 
-        mAllowedUid.push_back(1000);
-        mAllowedUid.push_back(0);
-        mAllowedUid.push_back(mStatsdUid);
+        mAllowedUid.push_back(AID_ROOT);
+        mAllowedUid.push_back(AID_STATSD);
+        mAllowedUid.push_back(AID_SYSTEM);
         mAllowedLogSources.insert(mAllowedUid.begin(), mAllowedUid.end());
     } else {
         for (const auto& source : config.allowed_log_source()) {
@@ -198,7 +199,7 @@
         // unless that caller is statsd itself (statsd is allowed to spoof uids).
         long appHookUid = event.GetLong(event.size()-2, &err);
         int32_t loggerUid = event.GetUid();
-        if (err != NO_ERROR || (loggerUid != appHookUid && loggerUid != mStatsdUid)) {
+        if (err != NO_ERROR || (loggerUid != appHookUid && loggerUid != AID_STATSD)) {
             VLOG("AppHook has invalid uid: claimed %ld but caller is %d", appHookUid, loggerUid);
             return;
         }
@@ -333,16 +334,6 @@
     return totalSize;
 }
 
-int32_t MetricsManager::getStatsdUid() {
-    auto suit = UidMap::sAidToUidMapping.find("AID_STATSD");
-    if (suit != UidMap::sAidToUidMapping.end()) {
-        return suit->second;
-    } else {
-        ALOGE("Statsd failed to find its own uid!");
-        return -1;
-    }
-}
-
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
diff --git a/cmds/statsd/src/metrics/MetricsManager.h b/cmds/statsd/src/metrics/MetricsManager.h
index a1220f9..d4b9102 100644
--- a/cmds/statsd/src/metrics/MetricsManager.h
+++ b/cmds/statsd/src/metrics/MetricsManager.h
@@ -75,9 +75,6 @@
 
     sp<UidMap> mUidMap;
 
-    // The uid of statsd.
-    const int32_t mStatsdUid;
-
     bool mConfigValid = false;
 
     // The uid log sources from StatsdConfig.
@@ -139,9 +136,6 @@
 
     void initLogSourceWhiteList();
 
-    // Fetches the uid of statsd from UidMap.
-    static int32_t getStatsdUid();
-
     // The metrics that don't need to be uploaded or even reported.
     std::set<int64_t> mNoReportMetricIds;
 
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 40ee834..df64401 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -923,6 +923,14 @@
      * {@link CameraCaptureSession#setRepeatingRequest} or
      * {@link CameraCaptureSession#setRepeatingBurst}</p>
      *
+     * <p>Individual physical camera settings will only be honored for camera session
+     * that was initialiazed with corresponding physical camera id output configuration
+     * {@link OutputConfiguration#setPhysicalCameraId} and the same output targets are
+     * also attached in the request by {@link CaptureRequest.Builder#addTarget}.</p>
+     *
+     * <p>The output is undefined for any logical camera streams in case valid physical camera
+     * settings are attached.</p>
+     *
      * @param templateType An enumeration selecting the use case for this request. Not all template
      * types are supported on every device. See the documentation for each template type for
      * details.
diff --git a/core/java/android/os/IServiceManager.java b/core/java/android/os/IServiceManager.java
index 2176a78..89bf7b9 100644
--- a/core/java/android/os/IServiceManager.java
+++ b/core/java/android/os/IServiceManager.java
@@ -76,9 +76,15 @@
     int DUMP_FLAG_PRIORITY_CRITICAL = 1 << 0;
     int DUMP_FLAG_PRIORITY_HIGH = 1 << 1;
     int DUMP_FLAG_PRIORITY_NORMAL = 1 << 2;
+    /**
+     * Services are by default registered with a DEFAULT dump priority. DEFAULT priority has the
+     * same priority as NORMAL priority but the services are not called with dump priority
+     * arguments.
+     */
+    int DUMP_FLAG_PRIORITY_DEFAULT = 1 << 3;
     int DUMP_FLAG_PRIORITY_ALL = DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PRIORITY_HIGH
-            | DUMP_FLAG_PRIORITY_NORMAL;
+            | DUMP_FLAG_PRIORITY_NORMAL | DUMP_FLAG_PRIORITY_DEFAULT;
     /* Allows services to dump sections in protobuf format. */
-    int DUMP_FLAG_PROTO = 1 << 3;
+    int DUMP_FLAG_PROTO = 1 << 4;
 
 }
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index 42ec315..3be76d6 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -83,7 +83,7 @@
      * @param service the service object
      */
     public static void addService(String name, IBinder service) {
-        addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
+        addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
     }
 
     /**
@@ -96,7 +96,7 @@
      * to access this service
      */
     public static void addService(String name, IBinder service, boolean allowIsolated) {
-        addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_NORMAL);
+        addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
     }
 
     /**
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 8c65041..567127a 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3692,6 +3692,20 @@
                 new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
 
         /**
+         * User-selected RTT mode
+         * 0 = OFF
+         * 1 = FULL
+         * 2 = VCO
+         * 3 = HCO
+         * Uses the same constants as TTY (e.g. {@link android.telecom.TelecomManager#TTY_MODE_OFF})
+         * @hide
+         */
+        public static final String RTT_CALLING_MODE = "rtt_calling_mode";
+
+        /** @hide */
+        public static final Validator RTT_CALLING_MODE_VALIDATOR = TTY_MODE_VALIDATOR;
+
+        /**
          * Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
          * boolean (1 or 0).
          */
@@ -4016,6 +4030,7 @@
             DTMF_TONE_WHEN_DIALING,
             DTMF_TONE_TYPE_WHEN_DIALING,
             HEARING_AID,
+            RTT_CALLING_MODE,
             TTY_MODE,
             MASTER_MONO,
             SOUND_EFFECTS_ENABLED,
@@ -4214,6 +4229,7 @@
             VALIDATORS.put(DTMF_TONE_TYPE_WHEN_DIALING, DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR);
             VALIDATORS.put(HEARING_AID, HEARING_AID_VALIDATOR);
             VALIDATORS.put(TTY_MODE, TTY_MODE_VALIDATOR);
+            VALIDATORS.put(RTT_CALLING_MODE, RTT_CALLING_MODE_VALIDATOR);
             VALIDATORS.put(NOTIFICATION_LIGHT_PULSE, NOTIFICATION_LIGHT_PULSE_VALIDATOR);
             VALIDATORS.put(POINTER_LOCATION, POINTER_LOCATION_VALIDATOR);
             VALIDATORS.put(SHOW_TOUCHES, SHOW_TOUCHES_VALIDATOR);
diff --git a/core/java/android/transition/TransitionUtils.java b/core/java/android/transition/TransitionUtils.java
index 084b79d..60b77bc 100644
--- a/core/java/android/transition/TransitionUtils.java
+++ b/core/java/android/transition/TransitionUtils.java
@@ -163,10 +163,14 @@
     public static Bitmap createViewBitmap(View view, Matrix matrix, RectF bounds,
             ViewGroup sceneRoot) {
         final boolean addToOverlay = !view.isAttachedToWindow();
+        ViewGroup parent = null;
+        int indexInParent = 0;
         if (addToOverlay) {
             if (sceneRoot == null || !sceneRoot.isAttachedToWindow()) {
                 return null;
             }
+            parent = (ViewGroup) view.getParent();
+            indexInParent = parent.indexOfChild(view);
             sceneRoot.getOverlay().add(view);
         }
         Bitmap bitmap = null;
@@ -190,6 +194,7 @@
         }
         if (addToOverlay) {
             sceneRoot.getOverlay().remove(view);
+            parent.addView(view, indexInParent);
         }
         return bitmap;
     }
diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java
index f0838a1..77c652e 100644
--- a/core/java/android/transition/Visibility.java
+++ b/core/java/android/transition/Visibility.java
@@ -402,8 +402,11 @@
                 // Becoming GONE
                 if (startView == endView) {
                     viewToKeep = endView;
-                } else {
+                } else if (mCanRemoveViews) {
                     overlayView = startView;
+                } else {
+                    overlayView = TransitionUtils.copyViewImage(sceneRoot, startView,
+                            (View) startView.getParent());
                 }
             }
         }
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index adb4dbf..85a579d 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -3017,7 +3017,7 @@
         }
 
         private final class UpgradeController {
-            private static final int SETTINGS_VERSION = 153;
+            private static final int SETTINGS_VERSION = 154;
 
             private final int mUserId;
 
@@ -3627,6 +3627,23 @@
                     currentVersion = 153;
                 }
 
+                if (currentVersion == 153) {
+                    // Version 154: Read notification badge configuration from config.
+                    // If user has already set the value, don't do anything.
+                    final SettingsState systemSecureSettings = getSecureSettingsLocked(userId);
+                    final Setting showNotificationBadges = systemSecureSettings.getSettingLocked(
+                            Settings.Secure.NOTIFICATION_BADGING);
+                    if (showNotificationBadges.isNull()) {
+                        final boolean defaultValue = getContext().getResources().getBoolean(
+                                com.android.internal.R.bool.config_notificationBadging);
+                        systemSecureSettings.insertSettingLocked(
+                                Secure.NOTIFICATION_BADGING,
+                                defaultValue ? "1" : "0",
+                                null, true, SettingsState.SYSTEM_PACKAGE_NAME);
+                    }
+                    currentVersion = 154;
+                }
+
                 // vXXX: Add new settings above this point.
 
                 if (currentVersion != newVersion) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
index 1897171..1da50ad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusIconContainer.java
@@ -88,7 +88,8 @@
      * Layout is happening from end -> start
      */
     private void calculateIconTranslations() {
-        float translationX = getWidth();
+        float width = getWidth();
+        float translationX = width;
         float contentStart = getPaddingStart();
         int childCount = getChildCount();
         // Underflow === don't show content until that index
@@ -133,6 +134,15 @@
                 }
             }
         }
+
+        // Stole this from NotificationIconContainer. Not optimal but keeps the layout logic clean
+        if (isLayoutRtl()) {
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                ViewState state = getViewStateFromChild(child);
+                state.xTranslation = width - state.xTranslation - child.getWidth();
+            }
+        }
     }
 
     private void applyIconStates() {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index a3a854d..99f36d0 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -778,8 +778,11 @@
     long mWaitForNetworkTimeoutMs;
 
     /**
-     * Helper class which parses out priority arguments and dumps sections according to their
-     * priority. If priority arguments are omitted, function calls the legacy dump command.
+     * Helper class which strips out priority and proto arguments then calls the dump function with
+     * the appropriate arguments. If priority arguments are omitted, function calls the legacy
+     * dump command.
+     * If priority arguments are omitted all sections are dumped, otherwise sections are dumped
+     * according to their priority.
      */
     private final PriorityDump.PriorityDumper mPriorityDumper = new PriorityDump.PriorityDumper() {
         @Override
@@ -791,24 +794,7 @@
 
         @Override
         public void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args, boolean asProto) {
-            if (asProto) {
-                doDump(fd, pw, new String[0], asProto);
-            } else {
-                doDump(fd, pw, new String[]{"settings"}, asProto);
-                doDump(fd, pw, new String[]{"intents"}, asProto);
-                doDump(fd, pw, new String[]{"broadcasts"}, asProto);
-                doDump(fd, pw, new String[]{"providers"}, asProto);
-                doDump(fd, pw, new String[]{"permissions"}, asProto);
-                doDump(fd, pw, new String[]{"services"}, asProto);
-                doDump(fd, pw, new String[]{"recents"}, asProto);
-                doDump(fd, pw, new String[]{"lastanr"}, asProto);
-                doDump(fd, pw, new String[]{"starter"}, asProto);
-                doDump(fd, pw, new String[]{"containers"}, asProto);
-                if (mAssociations.size() > 0) {
-                    doDump(fd, pw, new String[]{"associations"}, asProto);
-                }
-                doDump(fd, pw, new String[]{"processes"}, asProto);
-            }
+            doDump(fd, pw, new String[]{"-a", "--normal-priority"}, asProto);
         }
 
         @Override
@@ -15504,6 +15490,7 @@
         boolean dumpClient = false;
         boolean dumpCheckin = false;
         boolean dumpCheckinFormat = false;
+        boolean dumpNormalPriority = false;
         boolean dumpVisibleStacksOnly = false;
         boolean dumpFocusedStackOnly = false;
         String dumpPackage = null;
@@ -15536,6 +15523,8 @@
                 dumpCheckin = dumpCheckinFormat = true;
             } else if ("-C".equals(opt)) {
                 dumpCheckinFormat = true;
+            } else if ("--normal-priority".equals(opt)) {
+                dumpNormalPriority = true;
             } else if ("-h".equals(opt)) {
                 ActivityManagerShellCommand.dumpHelp(pw, true);
                 return;
@@ -15937,11 +15926,15 @@
                     pw.println("-------------------------------------------------------------------------------");
                 }
                 dumpActivityContainersLocked(pw);
-                pw.println();
-                if (dumpAll) {
-                    pw.println("-------------------------------------------------------------------------------");
+                // Activities section is dumped as part of the Critical priority dump. Exclude the
+                // section if priority is Normal.
+                if (!dumpNormalPriority){
+                    pw.println();
+                    if (dumpAll) {
+                        pw.println("-------------------------------------------------------------------------------");
+                    }
+                    dumpActivitiesLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
                 }
-                dumpActivitiesLocked(fd, pw, args, opti, dumpAll, dumpClient, dumpPackage);
                 if (mAssociations.size() > 0) {
                     pw.println();
                     if (dumpAll) {
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java
new file mode 100644
index 0000000..57a3d99
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertParsingException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+/** Exception thrown when parsing errors occur. */
+public class CertParsingException extends Exception {
+
+    public CertParsingException(String message) {
+        super(message);
+    }
+
+    public CertParsingException(Exception cause) {
+        super(cause);
+    }
+
+    public CertParsingException(String message, Exception cause) {
+        super(message, cause);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
new file mode 100644
index 0000000..985f5b6
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertUtils.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+import static javax.xml.xpath.XPathConstants.NODESET;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertPath;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderException;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertPathValidatorException;
+import java.security.cert.CertStore;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.PKIXParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+/** Utility functions related to parsing and validating public-key certificates. */
+final class CertUtils {
+
+    private static final String CERT_FORMAT = "X.509";
+    private static final String CERT_PATH_ALG = "PKIX";
+    private static final String CERT_STORE_ALG = "Collection";
+    private static final String SIGNATURE_ALG = "SHA256withRSA";
+
+    private CertUtils() {}
+
+    enum MustExist {
+        FALSE,
+        EXACTLY_ONE,
+        AT_LEAST_ONE,
+    }
+
+    /**
+     * Decodes a byte array containing an encoded X509 certificate.
+     *
+     * @param certBytes the byte array containing the encoded X509 certificate
+     * @return the decoded X509 certificate
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static X509Certificate decodeCert(byte[] certBytes) throws CertParsingException {
+        return decodeCert(new ByteArrayInputStream(certBytes));
+    }
+
+    /**
+     * Decodes an X509 certificate from an {@code InputStream}.
+     *
+     * @param inStream the input stream containing the encoded X509 certificate
+     * @return the decoded X509 certificate
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static X509Certificate decodeCert(InputStream inStream) throws CertParsingException {
+        CertificateFactory certFactory;
+        try {
+            certFactory = CertificateFactory.getInstance(CERT_FORMAT);
+        } catch (CertificateException e) {
+            // Should not happen, as X.509 is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            return (X509Certificate) certFactory.generateCertificate(inStream);
+        } catch (CertificateException e) {
+            throw new CertParsingException(e);
+        }
+    }
+
+    /**
+     * Parses a byte array as the content of an XML file, and returns the root node of the XML file.
+     *
+     * @param xmlBytes the byte array that is the XML file content
+     * @return the root node of the XML file
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static Element getXmlRootNode(byte[] xmlBytes) throws CertParsingException {
+        try {
+            Document document =
+                    DocumentBuilderFactory.newInstance()
+                            .newDocumentBuilder()
+                            .parse(new ByteArrayInputStream(xmlBytes));
+            document.getDocumentElement().normalize();
+            return document.getDocumentElement();
+        } catch (SAXException | ParserConfigurationException | IOException e) {
+            throw new CertParsingException(e);
+        }
+    }
+
+    /**
+     * Gets the text contents of certain XML child nodes, given a XML root node and a list of tags
+     * representing the path to locate the child nodes. The whitespaces and newlines in the text
+     * contents are stripped away.
+     *
+     * <p>For example, the list of tags [tag1, tag2, tag3] represents the XML tree like the
+     * following:
+     *
+     * <pre>
+     *   <root>
+     *     <tag1>
+     *       <tag2>
+     *         <tag3>abc</tag3>
+     *         <tag3>def</tag3>
+     *       </tag2>
+     *     </tag1>
+     *   <root>
+     * </pre>
+     *
+     * @param mustExist whether and how many nodes must exist. If the number of child nodes does not
+     *                  satisfy the requirement, CertParsingException will be thrown.
+     * @param rootNode  the root node that serves as the starting point to locate the child nodes
+     * @param nodeTags  the list of tags representing the relative path from the root node
+     * @return a list of strings that are the text contents of the child nodes
+     * @throws CertParsingException if any parsing error occurs
+     */
+    static List<String> getXmlNodeContents(MustExist mustExist, Element rootNode,
+            String... nodeTags)
+            throws CertParsingException {
+        String expression = String.join("/", nodeTags);
+
+        XPath xPath = XPathFactory.newInstance().newXPath();
+        NodeList nodeList;
+        try {
+            nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET);
+        } catch (XPathExpressionException e) {
+            throw new CertParsingException(e);
+        }
+
+        switch (mustExist) {
+            case FALSE:
+                break;
+
+            case EXACTLY_ONE:
+                if (nodeList.getLength() != 1) {
+                    throw new CertParsingException(
+                            "The XML file must contain exactly one node with the path "
+                                    + expression);
+                }
+                break;
+
+            case AT_LEAST_ONE:
+                if (nodeList.getLength() == 0) {
+                    throw new CertParsingException(
+                            "The XML file must contain at least one node with the path "
+                                    + expression);
+                }
+                break;
+
+            default:
+                throw new UnsupportedOperationException(
+                        "This enum value of MustExist is not supported: " + mustExist);
+        }
+
+        List<String> result = new ArrayList<>();
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            Node node = nodeList.item(i);
+            // Remove whitespaces and newlines.
+            result.add(node.getTextContent().replaceAll("\\s", ""));
+        }
+        return result;
+    }
+
+    /**
+     * Decodes a base64-encoded string.
+     *
+     * @param str the base64-encoded string
+     * @return the decoding decoding result
+     * @throws CertParsingException if the input string is not a properly base64-encoded string
+     */
+    static byte[] decodeBase64(String str) throws CertParsingException {
+        try {
+            return Base64.getDecoder().decode(str);
+        } catch (IllegalArgumentException e) {
+            throw new CertParsingException(e);
+        }
+    }
+
+    /**
+     * Verifies a public-key signature that is computed by RSA with SHA256.
+     *
+     * @param signerPublicKey the public key of the original signer
+     * @param signature       the public-key signature
+     * @param signedBytes     the bytes that have been signed
+     * @throws CertValidationException if the signature verification fails
+     */
+    static void verifyRsaSha256Signature(
+            PublicKey signerPublicKey, byte[] signature, byte[] signedBytes)
+            throws CertValidationException {
+        Signature verifier;
+        try {
+            verifier = Signature.getInstance(SIGNATURE_ALG);
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as SHA256withRSA is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            verifier.initVerify(signerPublicKey);
+            verifier.update(signedBytes);
+            if (!verifier.verify(signature)) {
+                throw new CertValidationException("The signature is invalid");
+            }
+        } catch (InvalidKeyException | SignatureException e) {
+            throw new CertValidationException(e);
+        }
+    }
+
+    /**
+     * Validates a leaf certificate, and returns the certificate path if the certificate is valid.
+     * If the given validation date is null, the current date will be used.
+     *
+     * @param validationDate    the date for which the validity of the certificate should be
+     *                          determined
+     * @param trustedRoot       the certificate of the trusted root CA
+     * @param intermediateCerts the list of certificates of possible intermediate CAs
+     * @param leafCert          the leaf certificate that is to be validated
+     * @return the certificate path if the leaf cert is valid
+     * @throws CertValidationException if {@code leafCert} is invalid (e.g., is expired, or has
+     *                                 invalid signature)
+     */
+    static CertPath validateCert(
+            @Nullable Date validationDate,
+            X509Certificate trustedRoot,
+            List<X509Certificate> intermediateCerts,
+            X509Certificate leafCert)
+            throws CertValidationException {
+        PKIXParameters pkixParams =
+                buildPkixParams(validationDate, trustedRoot, intermediateCerts, leafCert);
+        CertPath certPath = buildCertPath(pkixParams);
+
+        CertPathValidator certPathValidator;
+        try {
+            certPathValidator = CertPathValidator.getInstance(CERT_PATH_ALG);
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as PKIX is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            certPathValidator.validate(certPath, pkixParams);
+        } catch (CertPathValidatorException | InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+        return certPath;
+    }
+
+    @VisibleForTesting
+    static CertPath buildCertPath(PKIXParameters pkixParams) throws CertValidationException {
+        CertPathBuilder certPathBuilder;
+        try {
+            certPathBuilder = CertPathBuilder.getInstance(CERT_PATH_ALG);
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as PKIX is mandatory for all providers.
+            throw new RuntimeException(e);
+        }
+        try {
+            return certPathBuilder.build(pkixParams).getCertPath();
+        } catch (CertPathBuilderException | InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+    }
+
+    @VisibleForTesting
+    static PKIXParameters buildPkixParams(
+            @Nullable Date validationDate,
+            X509Certificate trustedRoot,
+            List<X509Certificate> intermediateCerts,
+            X509Certificate leafCert)
+            throws CertValidationException {
+        // Create a TrustAnchor from the trusted root certificate.
+        Set<TrustAnchor> trustedAnchors = new HashSet<>();
+        trustedAnchors.add(new TrustAnchor(trustedRoot, null));
+
+        // Create a CertStore from the list of intermediate certificates.
+        List<X509Certificate> certs = new ArrayList<>(intermediateCerts);
+        certs.add(leafCert);
+        CertStore certStore;
+        try {
+            certStore =
+                    CertStore.getInstance(CERT_STORE_ALG, new CollectionCertStoreParameters(certs));
+        } catch (NoSuchAlgorithmException e) {
+            // Should not happen, as Collection is mandatory for all providers.
+            throw new RuntimeException(e);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+
+        // Create a CertSelector from the leaf certificate.
+        X509CertSelector certSelector = new X509CertSelector();
+        certSelector.setCertificate(leafCert);
+
+        // Build a PKIXParameters from TrustAnchor, CertStore, and CertSelector.
+        PKIXBuilderParameters pkixParams;
+        try {
+            pkixParams = new PKIXBuilderParameters(trustedAnchors, certSelector);
+        } catch (InvalidAlgorithmParameterException e) {
+            throw new CertValidationException(e);
+        }
+        pkixParams.addCertStore(certStore);
+
+        // If validationDate is null, the current time will be used.
+        pkixParams.setDate(validationDate);
+        pkixParams.setRevocationEnabled(false);
+
+        return pkixParams;
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java
new file mode 100644
index 0000000..99a9ff7
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertValidationException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+/** Exception thrown when validation or verification fails. */
+public class CertValidationException extends Exception {
+
+    public CertValidationException(String message) {
+        super(message);
+    }
+
+    public CertValidationException(Exception cause) {
+        super(cause);
+    }
+
+    public CertValidationException(String message, Exception cause) {
+        super(message, cause);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java
new file mode 100644
index 0000000..2c04a86
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/CertXml.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.SecureRandom;
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+/**
+ * Parses and holds the XML file containing the list of THM public-key certificates and related
+ * metadata.
+ */
+public final class CertXml {
+
+    private static final String METADATA_NODE_TAG = "metadata";
+    private static final String METADATA_SERIAL_NODE_TAG = "serial";
+    private static final String METADATA_REFRESH_INTERVAL_NODE_TAG = "refresh-interval";
+    private static final String ENDPOINT_CERT_LIST_TAG = "endpoints";
+    private static final String ENDPOINT_CERT_ITEM_TAG = "cert";
+    private static final String INTERMEDIATE_CERT_LIST_TAG = "intermediates";
+    private static final String INTERMEDIATE_CERT_ITEM_TAG = "cert";
+
+    private final long serial;
+    private final long refreshInterval;
+    private final List<X509Certificate> intermediateCerts;
+    private final List<X509Certificate> endpointCerts;
+
+    private CertXml(
+            long serial,
+            long refreshInterval,
+            List<X509Certificate> intermediateCerts,
+            List<X509Certificate> endpointCerts) {
+        this.serial = serial;
+        this.refreshInterval = refreshInterval;
+        this.intermediateCerts = intermediateCerts;
+        this.endpointCerts = endpointCerts;
+    }
+
+    /** Gets the serial number of the XML file containing public-key certificates. */
+    public long getSerial() {
+        return serial;
+    }
+
+    /**
+     * Gets the refresh interval in the XML file containing public-key certificates. The refresh
+     * interval denotes the number of seconds that the client should follow to contact the server to
+     * refresh the XML file.
+     */
+    public long getRefreshInterval() {
+        return refreshInterval;
+    }
+
+    @VisibleForTesting
+    List<X509Certificate> getAllIntermediateCerts() {
+        return intermediateCerts;
+    }
+
+    @VisibleForTesting
+    List<X509Certificate> getAllEndpointCerts() {
+        return endpointCerts;
+    }
+
+    /**
+     * Chooses a random endpoint certificate from the XML file, validates the chosen certificate,
+     * and returns the certificate path including the chosen certificate if it is valid.
+     *
+     * @param trustedRoot the trusted root certificate
+     * @return the certificate path including the chosen certificate if the certificate is valid
+     * @throws CertValidationException if the chosen certificate cannot be validated based on the
+     *                                 trusted root certificate
+     */
+    public CertPath getRandomEndpointCert(X509Certificate trustedRoot)
+            throws CertValidationException {
+        return getEndpointCert(
+                new SecureRandom().nextInt(this.endpointCerts.size()),
+                /*validationDate=*/ null,
+                trustedRoot);
+    }
+
+    @VisibleForTesting
+    CertPath getEndpointCert(
+            int index, @Nullable Date validationDate, X509Certificate trustedRoot)
+            throws CertValidationException {
+        X509Certificate chosenCert = endpointCerts.get(index);
+        return CertUtils.validateCert(validationDate, trustedRoot, intermediateCerts, chosenCert);
+    }
+
+    /**
+     * Parses a byte array as the content of the XML file containing a list of endpoint
+     * certificates.
+     *
+     * @param bytes the bytes of the XML file
+     * @return a {@code CertXml} instance that contains the parsing result
+     * @throws CertParsingException if any parsing error occurs
+     */
+    public static CertXml parse(byte[] bytes) throws CertParsingException {
+        Element rootNode = CertUtils.getXmlRootNode(bytes);
+        return new CertXml(
+                parseSerial(rootNode),
+                parseRefreshInterval(rootNode),
+                parseIntermediateCerts(rootNode),
+                parseEndpointCerts(rootNode));
+    }
+
+    private static long parseSerial(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.EXACTLY_ONE,
+                        rootNode,
+                        METADATA_NODE_TAG,
+                        METADATA_SERIAL_NODE_TAG);
+        return Long.parseLong(contents.get(0));
+    }
+
+    private static long parseRefreshInterval(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.EXACTLY_ONE,
+                        rootNode,
+                        METADATA_NODE_TAG,
+                        METADATA_REFRESH_INTERVAL_NODE_TAG);
+        return Long.parseLong(contents.get(0));
+    }
+
+    private static List<X509Certificate> parseIntermediateCerts(Element rootNode)
+            throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.FALSE,
+                        rootNode,
+                        INTERMEDIATE_CERT_LIST_TAG,
+                        INTERMEDIATE_CERT_ITEM_TAG);
+        List<X509Certificate> res = new ArrayList<>();
+        for (String content : contents) {
+            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+        }
+        return Collections.unmodifiableList(res);
+    }
+
+    private static List<X509Certificate> parseEndpointCerts(Element rootNode)
+            throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.AT_LEAST_ONE,
+                        rootNode,
+                        ENDPOINT_CERT_LIST_TAG,
+                        ENDPOINT_CERT_ITEM_TAG);
+        List<X509Certificate> res = new ArrayList<>();
+        for (String content : contents) {
+            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+        }
+        return Collections.unmodifiableList(res);
+    }
+}
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java
new file mode 100644
index 0000000..878fc6e
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/certificate/SigXml.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+/**
+ * Parses and holds the XML file containing the signature of the XML file containing the list of THM
+ * public-key certificates.
+ */
+public final class SigXml {
+
+    private static final String INTERMEDIATE_CERT_LIST_TAG = "intermediates";
+    private static final String INTERMEDIATE_CERT_ITEM_TAG = "cert";
+    private static final String SIGNER_CERT_NODE_TAG = "certificate";
+    private static final String SIGNATURE_NODE_TAG = "value";
+
+    private final List<X509Certificate> intermediateCerts;
+    private final X509Certificate signerCert;
+    private final byte[] signature;
+
+    private SigXml(
+            List<X509Certificate> intermediateCerts, X509Certificate signerCert, byte[] signature) {
+        this.intermediateCerts = intermediateCerts;
+        this.signerCert = signerCert;
+        this.signature = signature;
+    }
+
+    /**
+     * Verifies the signature contained in this XML file against a trusted root certificate and the
+     * binary content of another file. The signer's public-key certificate and possible intermediate
+     * CA certificates are included in this XML file, and will be validated against the trusted root
+     * certificate.
+     *
+     * @param trustedRoot     the trusted root certificate
+     * @param signedFileBytes the original file content that has been signed
+     * @throws CertValidationException if the signature verification fails, or the signer's
+     *                                 certificate contained in this XML file cannot be validated
+     *                                 based on the trusted root certificate
+     */
+    public void verifyFileSignature(X509Certificate trustedRoot, byte[] signedFileBytes)
+            throws CertValidationException {
+        verifyFileSignature(trustedRoot, signedFileBytes, /*validationDate=*/ null);
+    }
+
+    @VisibleForTesting
+    void verifyFileSignature(
+            X509Certificate trustedRoot, byte[] signedFileBytes, @Nullable Date validationDate)
+            throws CertValidationException {
+        CertUtils.validateCert(validationDate, trustedRoot, intermediateCerts, signerCert);
+        CertUtils.verifyRsaSha256Signature(signerCert.getPublicKey(), signature, signedFileBytes);
+    }
+
+    /**
+     * Parses a byte array as the content of the XML file containing the signature and related
+     * certificates.
+     *
+     * @param bytes the bytes of the XML file
+     * @return a {@code SigXml} instance that contains the parsing result
+     * @throws CertParsingException if any parsing error occurs
+     */
+    public static SigXml parse(byte[] bytes) throws CertParsingException {
+        Element rootNode = CertUtils.getXmlRootNode(bytes);
+        return new SigXml(
+                parseIntermediateCerts(rootNode), parseSignerCert(rootNode),
+                parseFileSignature(rootNode));
+    }
+
+    private static List<X509Certificate> parseIntermediateCerts(Element rootNode)
+            throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.FALSE,
+                        rootNode,
+                        INTERMEDIATE_CERT_LIST_TAG,
+                        INTERMEDIATE_CERT_ITEM_TAG);
+        List<X509Certificate> res = new ArrayList<>();
+        for (String content : contents) {
+            res.add(CertUtils.decodeCert(CertUtils.decodeBase64(content)));
+        }
+        return Collections.unmodifiableList(res);
+    }
+
+    private static X509Certificate parseSignerCert(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.EXACTLY_ONE, rootNode, SIGNER_CERT_NODE_TAG);
+        return CertUtils.decodeCert(CertUtils.decodeBase64(contents.get(0)));
+    }
+
+    private static byte[] parseFileSignature(Element rootNode) throws CertParsingException {
+        List<String> contents =
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.EXACTLY_ONE, rootNode, SIGNATURE_NODE_TAG);
+        return CertUtils.decodeBase64(contents.get(0));
+    }
+}
diff --git a/services/core/java/com/android/server/stats/StatsCompanionService.java b/services/core/java/com/android/server/stats/StatsCompanionService.java
index f498cdd..c280739 100644
--- a/services/core/java/com/android/server/stats/StatsCompanionService.java
+++ b/services/core/java/com/android/server/stats/StatsCompanionService.java
@@ -113,7 +113,6 @@
         new StatFs(Environment.getRootDirectory().getAbsolutePath());
     private final StatFs mStatFsTemp =
         new StatFs(Environment.getDownloadCacheDirectory().getAbsolutePath());
-    private final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
 
     public StatsCompanionService(Context context) {
         super();
@@ -677,12 +676,13 @@
     }
 
     private synchronized BluetoothActivityEnergyInfo pullBluetoothData() {
+        final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
       if (adapter != null) {
-        SynchronousResultReceiver bluetoothReceiver = null;
-        bluetoothReceiver = new SynchronousResultReceiver("bluetooth");
+        SynchronousResultReceiver bluetoothReceiver = new SynchronousResultReceiver("bluetooth");
         adapter.requestControllerActivityEnergyInfo(bluetoothReceiver);
         return awaitControllerInfo(bluetoothReceiver);
       } else {
+          Slog.e(TAG, "Failed to get bluetooth adapter!");
         return null;
       }
     }
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem
new file mode 100644
index 0000000..b5d513c
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-1-no-begin-end.pem
@@ -0,0 +1,49 @@
+MIIJJzCCBQ6gAwIBAgIJAM7fBGeQ1wBkMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV
+BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMTEwNjQ5MzNaFw0zODAx
+MDYwNjQ5MzNaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCBCIw
+DQYJKoZIhvcNAQEBBQADggQPADCCBAoCggQBCcFv05M1seLPYIW7oFivh7u5otCt
+Mm7ryq0UjpbTQPcQxJQbYzcUQF7CbPYTdWDid4EuO8Zec03ownsduFhKud6H3yIQ
+4ueGiqCBoG1D6+N8fF9R8awTmAsbNg63VInx6IwcBZnjFlsIIftOFxqIJBYhiKhN
+JhPtF1i9dn+N5HjEKmkJO3pXhYhRXMp7OwL/epxzhBXFYT7aDg9pnaM6C+4hmhQ/
+0q2oyzYtAqFmrEenbtI6G47SzMc+shNTuYLtq21j/Z3uA3RwB9Szfu99F66tlgTX
+v7K7YS573hN3TQY/+nkLfFy/oF2LQRYvKHF+Nv0BHzQLzqDEYBaILcMf3i2Ce/b7
+wZjitLqFAI1swqGzgH/QpB3OrX51M/B7UCF2nB7Pa8knu4kBDGkz2Q41jAL0W/qt
+j43VwJDW0Y98OuqQiCqJrTrGdv7b/phnVVBvFrtIjYMfyK34jy5VLXctV5CSkWj5
+3ul3mvGFHJD+6nneDR4PUkmYN0khT4t/RqnQlwYE0a6Erq1+Rof6/DoWSzeBLBYV
+JaHhRy9mrudR/VcQynLKty6Zst4Lyh6aPMHcpTwGZbG+4mXnWeTaLEnGvivldksT
+XOxipcO/fXJfDss4b0glGzP3GD0+H5EZB9coYzNT47QZd9drxHdrLxtPoi+MeqkG
+gCdyFyBZO8G2k/JuyziT6hy+50VXJnl6Ujxj7MVUYAsISHsHgqETDsukQbbKvTKg
+3gxPVNN/vKWwyh7KLcFIaOEoPOgStkmVsqrXm7YLE6Bvzm8nu4rwJeAF9Yseg9BE
+Y86TRRmAI7fW4eDEPnxgCUUvcYSAh5mcayIyIr0KTuXkevwYbVRHMVmy9DaqzsP8
+YFXIqFvDXRCFSy/gMkoNb9ZoqdkmjZ+VBsjAKI+u/Haf6pgdpGZbVGKEFmaVHCkr
+tPp/gy4kE4qmd/SIaccG8o6Eb9X9fbqTTDZv34kcGgxOvBJVIaNHprTjgvYEnRaD
+KTlmZoCUmBlHzvbf68YWBmIz0K8vYPdx9r98LiUgpbTHtKZIYrJnbgPnbC9icP24
+2ksB4yaTx1QWc14vTNv1lUtv4zJEmaaoynNlETJFf/Tz0QKJxtT+l/BIAz8kEJMA
+cKsfoTx9OTtfuL85pXbCgxbKKmKn6RzxUCzSzgMboC0z6W8Zxy2gLIhqMm8AXAF7
+salwrRirV4lWsM9MOhVEgfjcv/qmQSYr1ARrwwegHRqxPA3qh11kfq5YSFU7W7+f
+JrWH6VuLZ0B1fj2+lsoMNekFA1ULD8DK7aAFIh9Y1y4Jt//xMuOPcD5PWNGFmUk7
+oPewiIUMLjXSWcgrQVYbZEDW/vooMJoo47Vg1fQPehejbONE1nBIaeRVhJcCAwEA
+AaNjMGEwHQYDVR0OBBYEFNd7oYeSi7hSGimRpTZaHLQy6+zRMB8GA1UdIwQYMBaA
+FNd7oYeSi7hSGimRpTZaHLQy6+zRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IEAgABny011veuPplTcNeyLeKhWRn/y9VM
+QEyhaLlsC1Di35WN8owjj4+gugLlDdtEc/pvzgk+ZkAuplQkrsU907dvwkDPb4rW
+ZB5hjbr9yVyEHK1bHh7RSUkJrB3NRwqH1lyWz/LfaVfbV4CtaaERhThzZCp/MweO
+Tivg2XpSluy5s8lEEbMC/dGuIsBMitX3XLlbYa2ND3aHZLo6X9yQMFfTCjgwYG2n
+eDYupnvcLINlrlJYcrYSrIvoQn9XfsnjU3AXz+jc6xLrO3EtXDhi9e+QTfcnvRsg
+l/Hj9SZr1w1L1PPJo+KjsRavVvzaHXlBAvvUtEojJrkR3j+b5zvQB6dgVrM0zUhM
+Q9zRp5R/xqHeZ/0TTQe9kEa8QuRzuRIkK5Wbh76Eix3S+2uTsbj462nk4E0oPR8p
+iYopS4ZEFEfrKW14HOph9ZscI4l/HfDmTNfgpyFl62UrvzVBnoz+sbhTgbPHPcCX
+OUrhmpz9I5oBkyEAZYunSvzY/9SXUsz6psXHJmVzLQcne/YQTtpWzV/wGD7cjjDl
+bfzsmGCfZ8jqPBoicl5IUVdyZsJgufEZHXxKZQ7wL7R6jKrj/GtCDey1Wr2QT8VX
+5JTk9cJQFjgjDWaAyCBpGEaQvYJcaOxk2D+Wap5ax8nUfW/99vVFA0EJKsSVVzw7
+daRty0UpfZsx2Sfzpg0mymmgB8+NY6t68dL5C/xxAv5mEQ8wGJmP45iQpo5T6LVV
+MrktLf5eIzxlALQIW/AgpSH9JKCqpItdxfisAIIs9e8XHbVJJA0Jde7rtAj+TUY0
+h00xSqyfSSbpcDJ9lIoSZOJvFQdWOxB8c3vZZGGhMuRFm06sUHvcHjo8KwnbqyOx
+DGjeqt6YWty6WcNin0WciR33vGHIzwVNxNnmuY308bNsMvY9jsmd37hdmmwnmQge
+7AIa7TMPjaKm0vV/1ztFSODWCI2K7klmL2MtOJMGfqUeOfjPANbS3lMJBAH9qxLM
+7Kng+nfqVtt+NG9MxcTbP80FkBa/6JxGgjjsiwDmhr2MTCYOK/eD+WZikMOieyvH
+m2vgxYCdWrhaGfc3t6oQ2YO+mXI7e6d3F3a90UUYkBIgje9zu0RLxnBBhuoRyGwv
+uQAlqgMDBZIzTO0Vnwew7KRLdzLhWbiikhi81q6Lg62aWjbdF6Ue6AVXch+dqmr+
+9aVt0Y6ETTS77nrQyglyLKIeNx6cEHDjETXlPYGbCAlrdKAdTA4ngnBZnzGQ/8zg
+tP9zvIJVA6cuOAn8GFEsrb7GN20QSDwyJWrYi6f+m64D9rOK4Jz4t+lEfjcfJeM/
+UcNlhmATcMHXWPCoKkOfll4PBc/Wigv1xYw70RZ4pai07LzJxNHYhvpE3Q==
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem
new file mode 100644
index 0000000..c412709
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-2-empty-block.pem
@@ -0,0 +1,2 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem
new file mode 100644
index 0000000..9137b16
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/invalid-cert-3-invalid-key.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem
new file mode 100644
index 0000000..e3abeaa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert-multiple-blocks.pem
@@ -0,0 +1,70 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+BuwwuQxvQDF4pmQd
+-----END CERTIFICATE-----
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem
new file mode 100644
index 0000000..5c16a1f
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/pem/valid-cert.pem
@@ -0,0 +1,204 @@
+-----BEGIN CERTIFICATE-----
+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
+-----END CERTIFICATE-----
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number:
+            ce:df:04:67:90:d7:00:64
+    Signature Algorithm: sha512WithRSAEncryption
+        Issuer: CN = Google CryptAuthVault
+        Validity
+            Not Before: Jan 11 06:49:33 2018 GMT
+            Not After : Jan  6 06:49:33 2038 GMT
+        Subject: CN = Google CryptAuthVault
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (8196 bit)
+                Modulus:
+                    09:c1:6f:d3:93:35:b1:e2:cf:60:85:bb:a0:58:af:
+                    87:bb:b9:a2:d0:ad:32:6e:eb:ca:ad:14:8e:96:d3:
+                    40:f7:10:c4:94:1b:63:37:14:40:5e:c2:6c:f6:13:
+                    75:60:e2:77:81:2e:3b:c6:5e:73:4d:e8:c2:7b:1d:
+                    b8:58:4a:b9:de:87:df:22:10:e2:e7:86:8a:a0:81:
+                    a0:6d:43:eb:e3:7c:7c:5f:51:f1:ac:13:98:0b:1b:
+                    36:0e:b7:54:89:f1:e8:8c:1c:05:99:e3:16:5b:08:
+                    21:fb:4e:17:1a:88:24:16:21:88:a8:4d:26:13:ed:
+                    17:58:bd:76:7f:8d:e4:78:c4:2a:69:09:3b:7a:57:
+                    85:88:51:5c:ca:7b:3b:02:ff:7a:9c:73:84:15:c5:
+                    61:3e:da:0e:0f:69:9d:a3:3a:0b:ee:21:9a:14:3f:
+                    d2:ad:a8:cb:36:2d:02:a1:66:ac:47:a7:6e:d2:3a:
+                    1b:8e:d2:cc:c7:3e:b2:13:53:b9:82:ed:ab:6d:63:
+                    fd:9d:ee:03:74:70:07:d4:b3:7e:ef:7d:17:ae:ad:
+                    96:04:d7:bf:b2:bb:61:2e:7b:de:13:77:4d:06:3f:
+                    fa:79:0b:7c:5c:bf:a0:5d:8b:41:16:2f:28:71:7e:
+                    36:fd:01:1f:34:0b:ce:a0:c4:60:16:88:2d:c3:1f:
+                    de:2d:82:7b:f6:fb:c1:98:e2:b4:ba:85:00:8d:6c:
+                    c2:a1:b3:80:7f:d0:a4:1d:ce:ad:7e:75:33:f0:7b:
+                    50:21:76:9c:1e:cf:6b:c9:27:bb:89:01:0c:69:33:
+                    d9:0e:35:8c:02:f4:5b:fa:ad:8f:8d:d5:c0:90:d6:
+                    d1:8f:7c:3a:ea:90:88:2a:89:ad:3a:c6:76:fe:db:
+                    fe:98:67:55:50:6f:16:bb:48:8d:83:1f:c8:ad:f8:
+                    8f:2e:55:2d:77:2d:57:90:92:91:68:f9:de:e9:77:
+                    9a:f1:85:1c:90:fe:ea:79:de:0d:1e:0f:52:49:98:
+                    37:49:21:4f:8b:7f:46:a9:d0:97:06:04:d1:ae:84:
+                    ae:ad:7e:46:87:fa:fc:3a:16:4b:37:81:2c:16:15:
+                    25:a1:e1:47:2f:66:ae:e7:51:fd:57:10:ca:72:ca:
+                    b7:2e:99:b2:de:0b:ca:1e:9a:3c:c1:dc:a5:3c:06:
+                    65:b1:be:e2:65:e7:59:e4:da:2c:49:c6:be:2b:e5:
+                    76:4b:13:5c:ec:62:a5:c3:bf:7d:72:5f:0e:cb:38:
+                    6f:48:25:1b:33:f7:18:3d:3e:1f:91:19:07:d7:28:
+                    63:33:53:e3:b4:19:77:d7:6b:c4:77:6b:2f:1b:4f:
+                    a2:2f:8c:7a:a9:06:80:27:72:17:20:59:3b:c1:b6:
+                    93:f2:6e:cb:38:93:ea:1c:be:e7:45:57:26:79:7a:
+                    52:3c:63:ec:c5:54:60:0b:08:48:7b:07:82:a1:13:
+                    0e:cb:a4:41:b6:ca:bd:32:a0:de:0c:4f:54:d3:7f:
+                    bc:a5:b0:ca:1e:ca:2d:c1:48:68:e1:28:3c:e8:12:
+                    b6:49:95:b2:aa:d7:9b:b6:0b:13:a0:6f:ce:6f:27:
+                    bb:8a:f0:25:e0:05:f5:8b:1e:83:d0:44:63:ce:93:
+                    45:19:80:23:b7:d6:e1:e0:c4:3e:7c:60:09:45:2f:
+                    71:84:80:87:99:9c:6b:22:32:22:bd:0a:4e:e5:e4:
+                    7a:fc:18:6d:54:47:31:59:b2:f4:36:aa:ce:c3:fc:
+                    60:55:c8:a8:5b:c3:5d:10:85:4b:2f:e0:32:4a:0d:
+                    6f:d6:68:a9:d9:26:8d:9f:95:06:c8:c0:28:8f:ae:
+                    fc:76:9f:ea:98:1d:a4:66:5b:54:62:84:16:66:95:
+                    1c:29:2b:b4:fa:7f:83:2e:24:13:8a:a6:77:f4:88:
+                    69:c7:06:f2:8e:84:6f:d5:fd:7d:ba:93:4c:36:6f:
+                    df:89:1c:1a:0c:4e:bc:12:55:21:a3:47:a6:b4:e3:
+                    82:f6:04:9d:16:83:29:39:66:66:80:94:98:19:47:
+                    ce:f6:df:eb:c6:16:06:62:33:d0:af:2f:60:f7:71:
+                    f6:bf:7c:2e:25:20:a5:b4:c7:b4:a6:48:62:b2:67:
+                    6e:03:e7:6c:2f:62:70:fd:b8:da:4b:01:e3:26:93:
+                    c7:54:16:73:5e:2f:4c:db:f5:95:4b:6f:e3:32:44:
+                    99:a6:a8:ca:73:65:11:32:45:7f:f4:f3:d1:02:89:
+                    c6:d4:fe:97:f0:48:03:3f:24:10:93:00:70:ab:1f:
+                    a1:3c:7d:39:3b:5f:b8:bf:39:a5:76:c2:83:16:ca:
+                    2a:62:a7:e9:1c:f1:50:2c:d2:ce:03:1b:a0:2d:33:
+                    e9:6f:19:c7:2d:a0:2c:88:6a:32:6f:00:5c:01:7b:
+                    b1:a9:70:ad:18:ab:57:89:56:b0:cf:4c:3a:15:44:
+                    81:f8:dc:bf:fa:a6:41:26:2b:d4:04:6b:c3:07:a0:
+                    1d:1a:b1:3c:0d:ea:87:5d:64:7e:ae:58:48:55:3b:
+                    5b:bf:9f:26:b5:87:e9:5b:8b:67:40:75:7e:3d:be:
+                    96:ca:0c:35:e9:05:03:55:0b:0f:c0:ca:ed:a0:05:
+                    22:1f:58:d7:2e:09:b7:ff:f1:32:e3:8f:70:3e:4f:
+                    58:d1:85:99:49:3b:a0:f7:b0:88:85:0c:2e:35:d2:
+                    59:c8:2b:41:56:1b:64:40:d6:fe:fa:28:30:9a:28:
+                    e3:b5:60:d5:f4:0f:7a:17:a3:6c:e3:44:d6:70:48:
+                    69:e4:55:84:97
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Key Identifier:
+                D7:7B:A1:87:92:8B:B8:52:1A:29:91:A5:36:5A:1C:B4:32:EB:EC:D1
+            X509v3 Authority Key Identifier:
+                keyid:D7:7B:A1:87:92:8B:B8:52:1A:29:91:A5:36:5A:1C:B4:32:EB:EC:D1
+
+            X509v3 Basic Constraints: critical
+                CA:TRUE
+            X509v3 Key Usage: critical
+                Digital Signature, Certificate Sign, CRL Sign
+    Signature Algorithm: sha512WithRSAEncryption
+         01:9f:2d:35:d6:f7:ae:3e:99:53:70:d7:b2:2d:e2:a1:59:19:
+         ff:cb:d5:4c:40:4c:a1:68:b9:6c:0b:50:e2:df:95:8d:f2:8c:
+         23:8f:8f:a0:ba:02:e5:0d:db:44:73:fa:6f:ce:09:3e:66:40:
+         2e:a6:54:24:ae:c5:3d:d3:b7:6f:c2:40:cf:6f:8a:d6:64:1e:
+         61:8d:ba:fd:c9:5c:84:1c:ad:5b:1e:1e:d1:49:49:09:ac:1d:
+         cd:47:0a:87:d6:5c:96:cf:f2:df:69:57:db:57:80:ad:69:a1:
+         11:85:38:73:64:2a:7f:33:07:8e:4e:2b:e0:d9:7a:52:96:ec:
+         b9:b3:c9:44:11:b3:02:fd:d1:ae:22:c0:4c:8a:d5:f7:5c:b9:
+         5b:61:ad:8d:0f:76:87:64:ba:3a:5f:dc:90:30:57:d3:0a:38:
+         30:60:6d:a7:78:36:2e:a6:7b:dc:2c:83:65:ae:52:58:72:b6:
+         12:ac:8b:e8:42:7f:57:7e:c9:e3:53:70:17:cf:e8:dc:eb:12:
+         eb:3b:71:2d:5c:38:62:f5:ef:90:4d:f7:27:bd:1b:20:97:f1:
+         e3:f5:26:6b:d7:0d:4b:d4:f3:c9:a3:e2:a3:b1:16:af:56:fc:
+         da:1d:79:41:02:fb:d4:b4:4a:23:26:b9:11:de:3f:9b:e7:3b:
+         d0:07:a7:60:56:b3:34:cd:48:4c:43:dc:d1:a7:94:7f:c6:a1:
+         de:67:fd:13:4d:07:bd:90:46:bc:42:e4:73:b9:12:24:2b:95:
+         9b:87:be:84:8b:1d:d2:fb:6b:93:b1:b8:f8:eb:69:e4:e0:4d:
+         28:3d:1f:29:89:8a:29:4b:86:44:14:47:eb:29:6d:78:1c:ea:
+         61:f5:9b:1c:23:89:7f:1d:f0:e6:4c:d7:e0:a7:21:65:eb:65:
+         2b:bf:35:41:9e:8c:fe:b1:b8:53:81:b3:c7:3d:c0:97:39:4a:
+         e1:9a:9c:fd:23:9a:01:93:21:00:65:8b:a7:4a:fc:d8:ff:d4:
+         97:52:cc:fa:a6:c5:c7:26:65:73:2d:07:27:7b:f6:10:4e:da:
+         56:cd:5f:f0:18:3e:dc:8e:30:e5:6d:fc:ec:98:60:9f:67:c8:
+         ea:3c:1a:22:72:5e:48:51:57:72:66:c2:60:b9:f1:19:1d:7c:
+         4a:65:0e:f0:2f:b4:7a:8c:aa:e3:fc:6b:42:0d:ec:b5:5a:bd:
+         90:4f:c5:57:e4:94:e4:f5:c2:50:16:38:23:0d:66:80:c8:20:
+         69:18:46:90:bd:82:5c:68:ec:64:d8:3f:96:6a:9e:5a:c7:c9:
+         d4:7d:6f:fd:f6:f5:45:03:41:09:2a:c4:95:57:3c:3b:75:a4:
+         6d:cb:45:29:7d:9b:31:d9:27:f3:a6:0d:26:ca:69:a0:07:cf:
+         8d:63:ab:7a:f1:d2:f9:0b:fc:71:02:fe:66:11:0f:30:18:99:
+         8f:e3:98:90:a6:8e:53:e8:b5:55:32:b9:2d:2d:fe:5e:23:3c:
+         65:00:b4:08:5b:f0:20:a5:21:fd:24:a0:aa:a4:8b:5d:c5:f8:
+         ac:00:82:2c:f5:ef:17:1d:b5:49:24:0d:09:75:ee:eb:b4:08:
+         fe:4d:46:34:87:4d:31:4a:ac:9f:49:26:e9:70:32:7d:94:8a:
+         12:64:e2:6f:15:07:56:3b:10:7c:73:7b:d9:64:61:a1:32:e4:
+         45:9b:4e:ac:50:7b:dc:1e:3a:3c:2b:09:db:ab:23:b1:0c:68:
+         de:aa:de:98:5a:dc:ba:59:c3:62:9f:45:9c:89:1d:f7:bc:61:
+         c8:cf:05:4d:c4:d9:e6:b9:8d:f4:f1:b3:6c:32:f6:3d:8e:c9:
+         9d:df:b8:5d:9a:6c:27:99:08:1e:ec:02:1a:ed:33:0f:8d:a2:
+         a6:d2:f5:7f:d7:3b:45:48:e0:d6:08:8d:8a:ee:49:66:2f:63:
+         2d:38:93:06:7e:a5:1e:39:f8:cf:00:d6:d2:de:53:09:04:01:
+         fd:ab:12:cc:ec:a9:e0:fa:77:ea:56:db:7e:34:6f:4c:c5:c4:
+         db:3f:cd:05:90:16:bf:e8:9c:46:82:38:ec:8b:00:e6:86:bd:
+         8c:4c:26:0e:2b:f7:83:f9:66:62:90:c3:a2:7b:2b:c7:9b:6b:
+         e0:c5:80:9d:5a:b8:5a:19:f7:37:b7:aa:10:d9:83:be:99:72:
+         3b:7b:a7:77:17:76:bd:d1:45:18:90:12:20:8d:ef:73:bb:44:
+         4b:c6:70:41:86:ea:11:c8:6c:2f:b9:00:25:aa:03:03:05:92:
+         33:4c:ed:15:9f:07:b0:ec:a4:4b:77:32:e1:59:b8:a2:92:18:
+         bc:d6:ae:8b:83:ad:9a:5a:36:dd:17:a5:1e:e8:05:57:72:1f:
+         9d:aa:6a:fe:f5:a5:6d:d1:8e:84:4d:34:bb:ee:7a:d0:ca:09:
+         72:2c:a2:1e:37:1e:9c:10:70:e3:11:35:e5:3d:81:9b:08:09:
+         6b:74:a0:1d:4c:0e:27:82:70:59:9f:31:90:ff:cc:e0:b4:ff:
+         73:bc:82:55:03:a7:2e:38:09:fc:18:51:2c:ad:be:c6:37:6d:
+         10:48:3c:32:25:6a:d8:8b:a7:fe:9b:ae:03:f6:b3:8a:e0:9c:
+         f8:b7:e9:44:7e:37:1f:25:e3:3f:51:c3:65:86:60:13:70:c1:
+         d7:58:f0:a8:2a:43:9f:96:5e:0f:05:cf:d6:8a:0b:f5:c5:8c:
+         3b:d1:16:78:a5:a8:b4:ec:bc:c9:c4:d1:d8:86:fa:44:dd
\ No newline at end of file
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml
new file mode 100644
index 0000000..6519f49
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-endpoint-cert.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml
new file mode 100644
index 0000000..3da0122
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-refresh-interval.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml
new file mode 100644
index 0000000..4370ff0
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-no-serial.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml
new file mode 100644
index 0000000..0f4e8a3
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-refresh-intervals.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml
new file mode 100644
index 0000000..a2685aa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-cert-file-two-serials.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml
new file mode 100644
index 0000000..5dc8ffa
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signature.sig.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+    R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+    NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+    YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+    tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+    4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+    tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+    HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+    GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+    UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+    33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+    7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+    hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+    79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+    M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+    JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+    BuwwuQxvQDF4pmQd
+  </certificate>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml
new file mode 100644
index 0000000..aa81295
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-no-signer-cert.sig.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml
new file mode 100644
index 0000000..09d0f44
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signatures.sig.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+    R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+    NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+    YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+    tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+    4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+    tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+    HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+    GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+    UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+    33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+    7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+    hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+    79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+    M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+    JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+    BuwwuQxvQDF4pmQd
+  </certificate>
+  <value>
+    VEVTVA==
+  </value>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml
new file mode 100644
index 0000000..44e8993
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/invalid-sig-file-two-signer-certs.sig.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    signer certificate 1
+  </certificate>
+  <certificate>
+    signer certificate 2
+  </certificate>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml
new file mode 100644
index 0000000..e59bf36
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file-no-intermediates.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml
new file mode 100644
index 0000000..be4893f
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-cert-file.xml
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<certificates>
+  <metadata>
+    <serial>
+      1000
+    </serial>
+    <creation-time>
+      1515697631
+    </creation-time>
+    <refresh-interval>
+      2592000
+    </refresh-interval>
+    <previous>
+      <serial>
+        0
+      </serial>
+      <hash>
+        47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
+      </hash>
+    </previous>
+  </metadata>
+  <intermediates>
+    <cert>
+      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
+      +a0fa5B3pxc8BN0LsHA0stT/Y2o=
+    </cert>
+    <cert>
+      MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa
+      Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs
+      dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+      AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX
+      TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9
+      f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ
+      EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf
+      lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE
+      hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU
+      0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD
+      K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
+      hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds
+      E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn
+      AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n
+      N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE
+      tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2
+      gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb
+      RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz
+      7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw
+      pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n
+      UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp
+      HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g
+    </cert>
+  </intermediates>
+  <endpoints>
+    <cert>
+      MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+      R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+      NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+      YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+      tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+      4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+      tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+      HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+      GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+      UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+      33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+      7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+      hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+      79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+      M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+      JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+      BuwwuQxvQDF4pmQd
+    </cert>
+    <cert>
+      MIICrDCCAZSgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOjE4MDYGA1UEAwwvR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZSBJbnRlcm1lZGlhdGUwHhcN
+      MTgwMTEyMDEwMzA5WhcNMTkwMTEyMDEwMzA5WjArMSkwJwYDVQQDDCBHb29nbGUg
+      Q3J5cHRBdXRoVmF1bHQgSW5zdGFuY2UgMjBZMBMGByqGSM49AgEGCCqGSM49AwEH
+      A0IABGhmBQyWdjsXKJRbkW4iIrvt6iqhX5t2XGt/vZS9CoOl0fs+EvJXo4kgrnx8
+      /8SGxz3pwRkFhY943QYy6a1gv/2jgZUwgZIwCQYDVR0TBAIwADAdBgNVHQ4EFgQU
+      xFmLyxUS2JHKURBtewBKRP6kQBgwVgYDVR0jBE8wTYAU0dKv4xTaEmdwHyox3tY8
+      H8XVDGKhMaQvMC0xKzApBgNVBAMMIkdvb2dsZSBDcnlwdEF1dGhWYXVsdCBJbnRl
+      cm1lZGlhdGWCAhAAMA4GA1UdDwEB/wQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEA
+      EJWpl7HU6LxukLqhw2tVZr7IRrKIucRk+RKaaiMx1Hx2jsTTskiJRiZas/xoPSqX
+      z1K5DVgI486i7HyqnWkGH5xVzCsv+rya5FOSTS3jVtgtoA4HFEqeeAcDowPDqVw3
+      yFTA55ukZnzVaPLpDfPqkhzWiuLQ/4fI6YCmOnWB8KtHTMdyGsDSAkpoxpok++NJ
+      Lu79BoBLe2ucjN383lTlieLxmrmHjF9ryYSQczcm0v6irMOMxEovw5iT4LHiEhbm
+      DfOPW909fe/s+K3TGZ3Q6U77x8g5k9dVovMgA4pFwtREtknFjeK1wXR3/eXGcP3W
+      0bMX1yTWYJQFWCG3DFoC5w==
+    </cert>
+    <cert>
+      MIIDkjCCAXqgAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+      Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDU2MjNa
+      Fw0yMDA3MTMwMDU2MjNaMCsxKTAnBgNVBAMMIEdvb2dsZSBDcnlwdEF1dGhWYXVs
+      dCBJbnN0YW5jZSAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEimGC4KEks4JL
+      83DNAAGCA5sKqqxqUPgV8gxgsijlMPL0TOtyJhJ2vSFIULEa0coVFbu+fAdxt3CV
+      DbzD0vWAmaOBiDCBhTAJBgNVHRMEAjAAMB0GA1UdDgQWBBTVeDueShnq1LCqkCFL
+      MAYtpxkCuDBJBgNVHSMEQjBAgBRE9RxHT7U3EP1vdjRzNYMrU7EseKEkpCIwIDEe
+      MBwGA1UEAwwVR29vZ2xlIENyeXB0QXV0aFZhdWx0ggIQATAOBgNVHQ8BAf8EBAMC
+      AwgwDQYJKoZIhvcNAQELBQADggIBADUZCjfBwBL9SSZDkMwE3n08schTBAgZCCrv
+      XOQVPGrfbFUcMv1mT4e8a5zxE98HsCS6K4HB40RtTXkmt6nuN+NyBrAmZJrBCqvG
+      IYtGsBLLEnojgWuSpQIBQeQy9it3RFdSR/1FIPssrWUB5KrtRvd+07+Mo7tI91jE
+      EunOrocu46g6p/OKSIZ7UmwZzczn2CJsrxuNPgqdlza3ytb+TTm536ZHnqaefSZD
+      rrruNneTXoqjC2OFn/OVLHQ1ee/vrHiX70P8p1V09cccDiwMCIZskNACYgWRdLYU
+      F5aYGueoFajrb4zmMoy8DK/4lh1EsfWMsrsQK6whmPidzgz37nb3rPpiHTdxu1Fc
+      2XM9QV3Jfj2U5FMOZTcDha7W7kb++gSnQEPTM0+Zu6lTJmcZgK4RJ7lmIfK/eVbJ
+      6V/wplOzXSxO3jBb2LNhLbhkUzcg68EIEPxaBpXYVOU2tSo2FMgi7/YLwQLorc6Q
+      h1pUZep8T8SLpvI02GvsB8TroFr2tCvCe5A1VxBQDx9IE7nEd2N30XxqReFk8Y82
+      xZMUOA4DL33NI45KWjhcawm0tzAPFfKjta/zYvnf7rwwE4r2PVuOXdet5eN8zBje
+      yJbEpjCemADujcwtYQ8hScyj/eCT2KNbZ9ibY2yrZEEuRQanq8CLAbpvSZYIHVhg
+      Clar7+38
+    </cert>
+  </endpoints>
+</certificates>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml
new file mode 100644
index 0000000..9c0adcd
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file-no-intermediates.sig.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <certificate>
+    MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi
+    R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1
+    NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW
+    YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu
+    tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl
+    4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21
+    tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu
+    HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr
+    GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb
+    UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe
+    33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5
+    7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ
+    hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa
+    79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4
+    M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf
+    JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp
+    BuwwuQxvQDF4pmQd
+  </certificate>
+  <value>
+    VEVTVA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml
new file mode 100644
index 0000000..f94b109
--- /dev/null
+++ b/services/tests/servicestests/assets/KeyStoreRecoveryControllerTest/xml/valid-sig-file.sig.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<signature>
+  <intermediates>
+    <cert>
+      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
+      +a0fa5B3pxc8BN0LsHA0stT/Y2o=
+    </cert>
+  </intermediates>
+  <certificate>
+    MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v
+    Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa
+    Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs
+    dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC
+    AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX
+    TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9
+    f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ
+    EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf
+    lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE
+    hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU
+    0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD
+    K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI
+    hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds
+    E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn
+    AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n
+    N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE
+    tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2
+    gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb
+    RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz
+    7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw
+    pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n
+    UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp
+    HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g
+  </certificate>
+  <value>
+    zELcMKbEb82mjWdhaV62Po4Tn/fEnHg7TMQzlz3cpDP3uzGKXg4fvCa+yDYrEYqm17uywOfFQpJs
+    pVjoUMINdnYogO44mul+E+m/klzSQN3GbmvYDtKpFsSGqsymyFSg8Bv2LeDLx2Pisc3sLUhxwKN4
+    8B6MwfZ3qUnRa5/ySk0bzEYYiRWsYR5oY7vK0kAI5c4Oi77E0W440FaEhnT7WxucFUnAhZbOSwXA
+    apE39BXu6ZbAPpTSc4f+uMErF7cRGbIODcAQJko6yjliBmvnCxj0ct7qzwUpgJojy/G5DLaVx3MF
+    Xnee9peYCrKFWpB+Z/7io0/Fqs/fDF7U25BsXA==
+  </value>
+</signature>
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java
new file mode 100644
index 0000000..ac6d293
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertUtilsTest.java
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.io.InputStream;
+import java.security.KeyPairGenerator;
+import java.security.PublicKey;
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.w3c.dom.Element;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CertUtilsTest {
+
+    private static final String XML_STR = ""
+            + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+            + "<!-- comment 1 -->"
+            + "<root>\n\n\n\r\r\r"
+            + "  <node1>\r\n\r\n"
+            + "    node1-1</node1>"
+            + "  <!-- comment 2 -->"
+            + "  <node1>node1-2"
+            + "    \n\r\n\r</node1>"
+            + "  <node2>"
+            + "    <node1>   node2-node1-1</node1>"
+            + "    <node1>node2-node1-2   </node1>"
+            + "    <!-- comment 3 -->"
+            + "    <node1>   node2-node1-3   </node1>"
+            + "  </node2>"
+            + "</root>";
+
+    private static final String SIGNED_STR = "abcdefg\n";
+    private static final String SIGNATURE_BASE64 = ""
+            + "KxBt9B3pwL3/59SrjTJTpuhc9JRxLOUNwNr3J4EEdXj5BqkYOUeXIOjyBGp8XaOnmuW8WmBxhko3"
+            + "yTR3/M9x0/pJuKDgqQSSFG+I56O/IAri7DmMBfY8QqcgiF8RaR86G7mWXUIdu8ixEtpKa//T4bN7"
+            + "c8Txvt96ApAcW0wJDihfCqDEXyi56pFCp+qEZuL4fS8iZtZTUkvxim1tb2/IsZ9OyDd9BWxp+JTs"
+            + "zihzH6xqnUCa1aELSUZnU8OzWGeuKpVDQDbDMtQpcxJ9o+6L6wO5vmQutZAulgw5gRPGhYWVs8+0"
+            + "ATdNEbv8TSomkXkZ3/lMYnmPXKmaHxcP4330DA==";
+    private static final PublicKey SIGNER_PUBLIC_KEY = TestData.INTERMEDIATE_CA_2.getPublicKey();
+
+    @Test
+    public void decodeCert_readPemFile_succeeds_singleBlock() throws Exception {
+        InputStream f = TestData.openTestFile("pem/valid-cert.pem");
+        X509Certificate cert = CertUtils.decodeCert(f);
+        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
+    }
+
+    @Test
+    public void decodeCert_readPemFile_succeeds_multipleBlocks() throws Exception {
+        InputStream in = TestData.openTestFile("pem/valid-cert-multiple-blocks.pem");
+        X509Certificate cert = CertUtils.decodeCert(in);
+        assertThat(cert).isEqualTo(TestData.ROOT_CA_TRUSTED);
+    }
+
+    @Test
+    public void decodeCert_readPemFile_throwsIfNoBeginEndLines() throws Exception {
+        InputStream in = TestData.openTestFile("pem/invalid-cert-1-no-begin-end.pem");
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+    }
+
+    @Test
+    public void decodeCert_readPemFile_throwsIfEmptyBlock() throws Exception {
+        InputStream in = TestData.openTestFile("pem/invalid-cert-2-empty-block.pem");
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+    }
+
+    @Test
+    public void decodeCert_readPemFile_throwsIfInvalidCert() throws Exception {
+        InputStream in = TestData.openTestFile("pem/invalid-cert-3-invalid-key.pem");
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(in));
+    }
+
+    @Test
+    public void decodeCert_readBytes_succeeds() throws Exception {
+        X509Certificate cert = CertUtils.decodeCert(TestData.INTERMEDIATE_CA_2.getEncoded());
+        assertThat(cert.getIssuerX500Principal().getName())
+                .isEqualTo("CN=Google CryptAuthVault Intermediate");
+    }
+
+    @Test
+    public void decodeCert_readBytes_throwsIfInvalidCert() throws Exception {
+        byte[] modifiedCertBytes = TestData.INTERMEDIATE_CA_1.getEncoded();
+        modifiedCertBytes[0] ^= (byte) 1;
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeCert(modifiedCertBytes));
+    }
+
+    @Test
+    public void decodeBase64_succeeds() throws Exception {
+        assertThat(CertUtils.decodeBase64("VEVTVA==")).isEqualTo("TEST".getBytes(UTF_8));
+    }
+
+    @Test
+    public void decodeBase64_succeedsIfEmptyInput() throws Exception {
+        assertThat(CertUtils.decodeBase64("")).hasLength(0);
+    }
+
+    @Test
+    public void decodeBase64_throwsIfInvalidInput() throws Exception {
+        assertThrows(CertParsingException.class, () -> CertUtils.decodeBase64("EVTVA=="));
+    }
+
+    @Test
+    public void getXmlRootNode_succeeds() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(root.getTagName()).isEqualTo("root");
+    }
+
+    @Test
+    public void getXmlRootNode_throwsIfEmptyInput() throws Exception {
+        assertThrows(CertParsingException.class, () -> CertUtils.getXmlRootNode(new byte[0]));
+    }
+
+    @Test
+    public void getXmlNodeContents_singleLevel_succeeds() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(CertUtils.getXmlNodeContents(CertUtils.MustExist.FALSE, root, "node1"))
+                .containsExactly("node1-1", "node1-2");
+    }
+
+    @Test
+    public void getXmlNodeContents_multipleLevels_succeeds() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(CertUtils.getXmlNodeContents(CertUtils.MustExist.FALSE, root, "node2", "node1"))
+                .containsExactly("node2-node1-1", "node2-node1-2", "node2-node1-3");
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistFalse_succeedsIfNotExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        assertThat(
+                CertUtils.getXmlNodeContents(
+                        CertUtils.MustExist.FALSE, root, "node2", "node-not-exist"))
+                .isEmpty();
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistAtLeastOne_throwsIfNotExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertUtils.getXmlNodeContents(
+                                        CertUtils.MustExist.AT_LEAST_ONE, root, "node2",
+                                        "node-not-exist"));
+        assertThat(expected.getMessage()).contains("must contain at least one");
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistExactlyOne_throwsIfNotExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertUtils.getXmlNodeContents(
+                                        CertUtils.MustExist.EXACTLY_ONE, root, "node-not-exist",
+                                        "node1"));
+        assertThat(expected.getMessage()).contains("must contain exactly one");
+    }
+
+    @Test
+    public void getXmlNodeContents_mustExistExactlyOne_throwsIfMultipleExist() throws Exception {
+        Element root = CertUtils.getXmlRootNode(XML_STR.getBytes(UTF_8));
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertUtils.getXmlNodeContents(
+                                        CertUtils.MustExist.EXACTLY_ONE, root, "node2", "node1"));
+        assertThat(expected.getMessage()).contains("must contain exactly one");
+    }
+
+    @Test
+    public void verifyRsaSha256Signature_succeeds() throws Exception {
+        CertUtils.verifyRsaSha256Signature(
+                SIGNER_PUBLIC_KEY,
+                Base64.getDecoder().decode(SIGNATURE_BASE64),
+                SIGNED_STR.getBytes(UTF_8));
+    }
+
+    @Test
+    public void verifyRsaSha256Signature_throwsIfMismatchSignature() throws Exception {
+        byte[] modifiedBytes = SIGNED_STR.getBytes(UTF_8);
+        modifiedBytes[0] ^= (byte) 1;
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.verifyRsaSha256Signature(
+                                SIGNER_PUBLIC_KEY, Base64.getDecoder().decode(SIGNATURE_BASE64),
+                                modifiedBytes));
+    }
+
+    @Test
+    public void verifyRsaSha256Signature_throwsIfWrongKeyType() throws Exception {
+        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
+        keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
+        PublicKey publicKey = keyPairGenerator.generateKeyPair().getPublic();
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.verifyRsaSha256Signature(
+                                publicKey,
+                                Base64.getDecoder().decode(SIGNATURE_BASE64),
+                                SIGNED_STR.getBytes(UTF_8)));
+    }
+
+    @Test
+    public void buildCertPath_succeedsWithoutIntermediates() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        X509Certificate leafCert = TestData.INTERMEDIATE_CA_1;
+        CertPath certPath = CertUtils.buildCertPath(
+                CertUtils.buildPkixParams(
+                        TestData.DATE_ALL_CERTS_VALID, rootCert, Collections.emptyList(),
+                        leafCert));
+        assertThat(certPath.getCertificates()).containsExactly(
+                TestData.INTERMEDIATE_CA_1).inOrder();
+    }
+
+    @Test
+    public void buildCertPath_succeedsWithIntermediates() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        CertPath certPath =
+                CertUtils.buildCertPath(
+                        CertUtils.buildPkixParams(
+                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                leafCert));
+        assertThat(certPath.getCertificates())
+                .containsExactly(
+                        TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
+                        TestData.INTERMEDIATE_CA_1)
+                .inOrder();
+    }
+
+    @Test
+    public void buildCertPath_succeedsWithIntermediates_ignoreUnrelatedIntermedateCert()
+            throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_1;
+        CertPath certPath =
+                CertUtils.buildCertPath(
+                        CertUtils.buildPkixParams(
+                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                leafCert));
+        assertThat(certPath.getCertificates())
+                .containsExactly(TestData.LEAF_CERT_1, TestData.INTERMEDIATE_CA_1)
+                .inOrder();
+    }
+
+    @Test
+    public void buildCertPath_throwsIfWrongRootCommonName() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_1;
+
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.buildCertPath(
+                                CertUtils.buildPkixParams(
+                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                        leafCert)));
+    }
+
+    @Test
+    public void buildCertPath_throwsIfMissingIntermediateCert() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_COMMON_NAME;
+        List<X509Certificate> intermediateCerts = Collections.emptyList();
+        X509Certificate leafCert = TestData.LEAF_CERT_1;
+
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.buildCertPath(
+                                CertUtils.buildPkixParams(
+                                        TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                        leafCert)));
+    }
+
+    @Test
+    public void validateCert_succeeds() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        CertUtils.validateCert(TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                leafCert);
+    }
+
+    @Test
+    public void validateCert_throwsIfExpired() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_TRUSTED;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.validateCert(
+                                TestData.DATE_LEAF_CERT_2_EXPIRED, rootCert, intermediateCerts,
+                                leafCert));
+    }
+
+    @Test
+    public void validateCert_throwsIfWrongRootWithTheSameCommonName() throws Exception {
+        X509Certificate rootCert = TestData.ROOT_CA_DIFFERENT_KEY;
+        List<X509Certificate> intermediateCerts =
+                Arrays.asList(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2);
+        X509Certificate leafCert = TestData.LEAF_CERT_2;
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        CertUtils.validateCert(
+                                TestData.DATE_ALL_CERTS_VALID, rootCert, intermediateCerts,
+                                leafCert));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java
new file mode 100644
index 0000000..52269d9
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/CertXmlTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.security.cert.CertPath;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class CertXmlTest {
+
+    private byte[] certXmlBytes;
+
+    @Before
+    public void setUp() throws Exception {
+        certXmlBytes = TestData.readTestFile("xml/valid-cert-file.xml");
+    }
+
+    @Test
+    public void parse_succeeds() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThat(certXml.getSerial()).isEqualTo(1000L);
+        assertThat(certXml.getRefreshInterval()).isEqualTo(2592000L);
+    }
+
+    @Test
+    public void parse_succeedsIfNoIntermediateCerts() throws Exception {
+        CertXml certXml =
+                CertXml.parse(TestData.readTestFile("xml/valid-cert-file-no-intermediates.xml"));
+        assertThat(certXml.getAllIntermediateCerts()).isEmpty();
+    }
+
+    @Test
+    public void parse_checkIntermediateCerts() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        List<X509Certificate> intermediateCerts = certXml.getAllIntermediateCerts();
+        assertThat(intermediateCerts)
+                .containsExactly(TestData.INTERMEDIATE_CA_1, TestData.INTERMEDIATE_CA_2)
+                .inOrder();
+    }
+
+    @Test
+    public void parse_checkEndpointCerts() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        List<X509Certificate> endpointCerts = certXml.getAllEndpointCerts();
+        assertThat(endpointCerts).hasSize(3);
+        assertThat(endpointCerts).containsAllOf(TestData.LEAF_CERT_1, TestData.LEAF_CERT_2);
+    }
+
+    @Test
+    public void parse_throwsIfNoEndpointCert() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-no-endpoint-cert.xml")));
+        assertThat(expected.getMessage()).contains("at least one");
+    }
+
+    @Test
+    public void parse_throwsIfNoRefreshInterval() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-no-refresh-interval.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfNoSerial() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-no-serial.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoRefreshIntervals() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-two-refresh-intervals"
+                                                        + ".xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoSerials() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                CertXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-cert-file-two-serials.xml")));
+        assertThat(expected.getMessage()).contains("exactly one node");
+    }
+
+    @Test
+    public void parseAndValidateAllCerts_succeeds() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        for (int index = 0; index < certXml.getAllEndpointCerts().size(); index++) {
+            assertThat(
+                    certXml.getEndpointCert(
+                            index, TestData.DATE_ALL_CERTS_VALID, TestData.ROOT_CA_TRUSTED))
+                    .isNotNull();
+        }
+    }
+
+    @Test
+    public void parseAndValidate_returnsExpectedCertPath() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        CertPath certPath =
+                certXml.getEndpointCert(
+                        /*index=*/ 1, // TestData.LEAF_CERT_2
+                        TestData.DATE_ALL_CERTS_VALID,
+                        TestData.ROOT_CA_TRUSTED);
+        assertThat(certPath.getCertificates())
+                .containsExactly(
+                        TestData.LEAF_CERT_2, TestData.INTERMEDIATE_CA_2,
+                        TestData.INTERMEDIATE_CA_1)
+                .inOrder();
+    }
+
+    @Test
+    public void validateCert_throwsIfRootCertWithDifferentCommonName() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        certXml.getEndpointCert(
+                                /*index=*/ 0, // TestData.LEAF_CERT_1
+                                TestData.DATE_ALL_CERTS_VALID,
+                                TestData.ROOT_CA_DIFFERENT_COMMON_NAME));
+    }
+
+    @Test
+    public void validateCert_throwsIfRootCertWithDifferentPublicKey() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        certXml.getEndpointCert(
+                                /*index=*/ 0, // TestData.LEAF_CERT_1
+                                TestData.DATE_ALL_CERTS_VALID,
+                                TestData.ROOT_CA_DIFFERENT_KEY));
+    }
+
+    @Test
+    public void validateCert_throwsIfExpired() throws Exception {
+        CertXml certXml = CertXml.parse(certXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        certXml.getEndpointCert(
+                                /*index=*/ 1, // TestData.LEAF_CERT_2
+                                TestData.DATE_LEAF_CERT_2_EXPIRED,
+                                TestData.ROOT_CA_TRUSTED));
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java
new file mode 100644
index 0000000..4d87006
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/SigXmlTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+import static org.testng.Assert.expectThrows;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class SigXmlTest {
+
+    private byte[] certXmlBytes;
+    private byte[] sigXmlBytes;
+
+    @Before
+    public void setUp() throws Exception {
+        certXmlBytes = TestData.readTestFile("xml/valid-cert-file.xml");
+        sigXmlBytes = TestData.readTestFile("xml/valid-sig-file.sig.xml");
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_succeeds() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        sigXml.verifyFileSignature(
+                TestData.ROOT_CA_TRUSTED, certXmlBytes, TestData.DATE_ALL_CERTS_VALID);
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_throwsIfExpiredCert() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        sigXml.verifyFileSignature(
+                                TestData.ROOT_CA_TRUSTED, certXmlBytes,
+                                TestData.DATE_INTERMEDIATE_CA_2_EXPIRED));
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_throwsIfInvalidSignature() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        byte[] modifiedBytes = sigXmlBytes.clone();
+        modifiedBytes[0] ^= (byte) 1; // Flip one bit
+        CertValidationException expected =
+                expectThrows(
+                        CertValidationException.class,
+                        () ->
+                                sigXml.verifyFileSignature(
+                                        TestData.ROOT_CA_TRUSTED, modifiedBytes,
+                                        TestData.DATE_ALL_CERTS_VALID));
+        assertThat(expected.getMessage()).contains("signature is invalid");
+    }
+
+    @Test
+    public void parseAndVerifyFileSignature_throwsIfRootCertWithWrongCommonName() throws Exception {
+        SigXml sigXml = SigXml.parse(sigXmlBytes);
+        assertThrows(
+                CertValidationException.class,
+                () ->
+                        sigXml.verifyFileSignature(
+                                TestData.ROOT_CA_DIFFERENT_COMMON_NAME,
+                                certXmlBytes,
+                                TestData.DATE_ALL_CERTS_VALID));
+    }
+
+    @Test
+    public void parse_succeedsWithoutIntermediateCerts() throws Exception {
+        SigXml.parse(TestData.readTestFile("xml/valid-sig-file-no-intermediates.sig.xml"));
+    }
+
+    @Test
+    public void parse_throwsIfNoSignerCert() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-no-signer-cert.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoSignerCerts() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-two-signer-certs.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfNoSignatureValue() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-no-signature.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+
+    @Test
+    public void parse_throwsIfTwoSignatureValues() throws Exception {
+        CertParsingException expected =
+                expectThrows(
+                        CertParsingException.class,
+                        () ->
+                                SigXml.parse(
+                                        TestData.readTestFile(
+                                                "xml/invalid-sig-file-two-signatures.sig.xml")));
+        assertThat(expected.getMessage()).contains("exactly one");
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java
new file mode 100644
index 0000000..5eb4166
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/recoverablekeystore/certificate/TestData.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2017 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.server.locksettings.recoverablekeystore.certificate;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+
+import com.google.common.io.ByteStreams;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Date;
+
+/** Constants used by the tests in the folder. */
+public final class TestData {
+
+    private static final String TEST_FILE_FOLDER_NAME = "KeyStoreRecoveryControllerTest";
+
+    private TestData() {}
+
+    // Some test data that is generated by using OpenSSL command line tools.
+    private static final String ROOT_CA_TRUSTED_BASE64 = ""
+            + "MIIJJzCCBQ6gAwIBAgIJAM7fBGeQ1wBkMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV"
+            + "BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMTEwNjQ5MzNaFw0zODAx"
+            + "MDYwNjQ5MzNaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCBCIw"
+            + "DQYJKoZIhvcNAQEBBQADggQPADCCBAoCggQBCcFv05M1seLPYIW7oFivh7u5otCt"
+            + "Mm7ryq0UjpbTQPcQxJQbYzcUQF7CbPYTdWDid4EuO8Zec03ownsduFhKud6H3yIQ"
+            + "4ueGiqCBoG1D6+N8fF9R8awTmAsbNg63VInx6IwcBZnjFlsIIftOFxqIJBYhiKhN"
+            + "JhPtF1i9dn+N5HjEKmkJO3pXhYhRXMp7OwL/epxzhBXFYT7aDg9pnaM6C+4hmhQ/"
+            + "0q2oyzYtAqFmrEenbtI6G47SzMc+shNTuYLtq21j/Z3uA3RwB9Szfu99F66tlgTX"
+            + "v7K7YS573hN3TQY/+nkLfFy/oF2LQRYvKHF+Nv0BHzQLzqDEYBaILcMf3i2Ce/b7"
+            + "wZjitLqFAI1swqGzgH/QpB3OrX51M/B7UCF2nB7Pa8knu4kBDGkz2Q41jAL0W/qt"
+            + "j43VwJDW0Y98OuqQiCqJrTrGdv7b/phnVVBvFrtIjYMfyK34jy5VLXctV5CSkWj5"
+            + "3ul3mvGFHJD+6nneDR4PUkmYN0khT4t/RqnQlwYE0a6Erq1+Rof6/DoWSzeBLBYV"
+            + "JaHhRy9mrudR/VcQynLKty6Zst4Lyh6aPMHcpTwGZbG+4mXnWeTaLEnGvivldksT"
+            + "XOxipcO/fXJfDss4b0glGzP3GD0+H5EZB9coYzNT47QZd9drxHdrLxtPoi+MeqkG"
+            + "gCdyFyBZO8G2k/JuyziT6hy+50VXJnl6Ujxj7MVUYAsISHsHgqETDsukQbbKvTKg"
+            + "3gxPVNN/vKWwyh7KLcFIaOEoPOgStkmVsqrXm7YLE6Bvzm8nu4rwJeAF9Yseg9BE"
+            + "Y86TRRmAI7fW4eDEPnxgCUUvcYSAh5mcayIyIr0KTuXkevwYbVRHMVmy9DaqzsP8"
+            + "YFXIqFvDXRCFSy/gMkoNb9ZoqdkmjZ+VBsjAKI+u/Haf6pgdpGZbVGKEFmaVHCkr"
+            + "tPp/gy4kE4qmd/SIaccG8o6Eb9X9fbqTTDZv34kcGgxOvBJVIaNHprTjgvYEnRaD"
+            + "KTlmZoCUmBlHzvbf68YWBmIz0K8vYPdx9r98LiUgpbTHtKZIYrJnbgPnbC9icP24"
+            + "2ksB4yaTx1QWc14vTNv1lUtv4zJEmaaoynNlETJFf/Tz0QKJxtT+l/BIAz8kEJMA"
+            + "cKsfoTx9OTtfuL85pXbCgxbKKmKn6RzxUCzSzgMboC0z6W8Zxy2gLIhqMm8AXAF7"
+            + "salwrRirV4lWsM9MOhVEgfjcv/qmQSYr1ARrwwegHRqxPA3qh11kfq5YSFU7W7+f"
+            + "JrWH6VuLZ0B1fj2+lsoMNekFA1ULD8DK7aAFIh9Y1y4Jt//xMuOPcD5PWNGFmUk7"
+            + "oPewiIUMLjXSWcgrQVYbZEDW/vooMJoo47Vg1fQPehejbONE1nBIaeRVhJcCAwEA"
+            + "AaNjMGEwHQYDVR0OBBYEFNd7oYeSi7hSGimRpTZaHLQy6+zRMB8GA1UdIwQYMBaA"
+            + "FNd7oYeSi7hSGimRpTZaHLQy6+zRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/"
+            + "BAQDAgGGMA0GCSqGSIb3DQEBDQUAA4IEAgABny011veuPplTcNeyLeKhWRn/y9VM"
+            + "QEyhaLlsC1Di35WN8owjj4+gugLlDdtEc/pvzgk+ZkAuplQkrsU907dvwkDPb4rW"
+            + "ZB5hjbr9yVyEHK1bHh7RSUkJrB3NRwqH1lyWz/LfaVfbV4CtaaERhThzZCp/MweO"
+            + "Tivg2XpSluy5s8lEEbMC/dGuIsBMitX3XLlbYa2ND3aHZLo6X9yQMFfTCjgwYG2n"
+            + "eDYupnvcLINlrlJYcrYSrIvoQn9XfsnjU3AXz+jc6xLrO3EtXDhi9e+QTfcnvRsg"
+            + "l/Hj9SZr1w1L1PPJo+KjsRavVvzaHXlBAvvUtEojJrkR3j+b5zvQB6dgVrM0zUhM"
+            + "Q9zRp5R/xqHeZ/0TTQe9kEa8QuRzuRIkK5Wbh76Eix3S+2uTsbj462nk4E0oPR8p"
+            + "iYopS4ZEFEfrKW14HOph9ZscI4l/HfDmTNfgpyFl62UrvzVBnoz+sbhTgbPHPcCX"
+            + "OUrhmpz9I5oBkyEAZYunSvzY/9SXUsz6psXHJmVzLQcne/YQTtpWzV/wGD7cjjDl"
+            + "bfzsmGCfZ8jqPBoicl5IUVdyZsJgufEZHXxKZQ7wL7R6jKrj/GtCDey1Wr2QT8VX"
+            + "5JTk9cJQFjgjDWaAyCBpGEaQvYJcaOxk2D+Wap5ax8nUfW/99vVFA0EJKsSVVzw7"
+            + "daRty0UpfZsx2Sfzpg0mymmgB8+NY6t68dL5C/xxAv5mEQ8wGJmP45iQpo5T6LVV"
+            + "MrktLf5eIzxlALQIW/AgpSH9JKCqpItdxfisAIIs9e8XHbVJJA0Jde7rtAj+TUY0"
+            + "h00xSqyfSSbpcDJ9lIoSZOJvFQdWOxB8c3vZZGGhMuRFm06sUHvcHjo8KwnbqyOx"
+            + "DGjeqt6YWty6WcNin0WciR33vGHIzwVNxNnmuY308bNsMvY9jsmd37hdmmwnmQge"
+            + "7AIa7TMPjaKm0vV/1ztFSODWCI2K7klmL2MtOJMGfqUeOfjPANbS3lMJBAH9qxLM"
+            + "7Kng+nfqVtt+NG9MxcTbP80FkBa/6JxGgjjsiwDmhr2MTCYOK/eD+WZikMOieyvH"
+            + "m2vgxYCdWrhaGfc3t6oQ2YO+mXI7e6d3F3a90UUYkBIgje9zu0RLxnBBhuoRyGwv"
+            + "uQAlqgMDBZIzTO0Vnwew7KRLdzLhWbiikhi81q6Lg62aWjbdF6Ue6AVXch+dqmr+"
+            + "9aVt0Y6ETTS77nrQyglyLKIeNx6cEHDjETXlPYGbCAlrdKAdTA4ngnBZnzGQ/8zg"
+            + "tP9zvIJVA6cuOAn8GFEsrb7GN20QSDwyJWrYi6f+m64D9rOK4Jz4t+lEfjcfJeM/"
+            + "UcNlhmATcMHXWPCoKkOfll4PBc/Wigv1xYw70RZ4pai07LzJxNHYhvpE3Q==";
+    private static final String ROOT_CA_DIFFERENT_KEY_BASE64 = ""
+            + "MIIFJjCCAw6gAwIBAgIJANpazyIWdcb/MA0GCSqGSIb3DQEBCwUAMCAxHjAcBgNV"
+            + "BAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDAeFw0xODAxMjEwMzI0MjVaFw0zODAx"
+            + "MTYwMzI0MjVaMCAxHjAcBgNVBAMMFUdvb2dsZSBDcnlwdEF1dGhWYXVsdDCCAiIw"
+            + "DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKkAUqkRxdVy9UpI9BjQnylGVPRW"
+            + "aQsCwT2iWJ7fuCnQQon1U9nOyw2R5GYKcA8Zy+4Co++6nzRblYYXJG3Fzsj+kxei"
+            + "pZGmU11djRJDOhHPPe5jSW37Y0czWaj8jx4xvMim18dGYR7fg6SsOOYXA2y5tlvZ"
+            + "xLjvw5qpL62J5bVoAAxjng/Oc2Osu+vpv6M50pUZr0OEiFi59WlwrCCZpf1/80bT"
+            + "j2ebCKKAtTYa6+Q+oMGGxb3imSRpmQPtFcvhUPmAaocUjYM/FeIGNRv14oED/aXz"
+            + "khuPb+QNkXgk9yiokE10IeAk6oNUNDyuiMNIFy67lUrwc45lv9y0s/8fHj9pvKse"
+            + "n3+UOKAuV9atUXLdFKQwnQPt4SOmHPkXoj+5tv32RSvVeYhb0ZOpQPkRhxv4wgs7"
+            + "NldNbKhzVDM9K4M5Q2TrPK1yJKrc5/z0bDzmPOcH4AAXPvSt5PZOs0NlXUJ99BcA"
+            + "KE1sWArUhipz5mx0hxPTNEM9/8bMb//HkbZtx2log1/fc207W/AFd2FICOpRY+Sb"
+            + "CJs9WjstpisppotONvgXxZbZiypGKxpeZOb4s6y3iXtZ0FWXUZrc65b8S06fDVCa"
+            + "ZomNFDWhspHFKyueBgU6cR9K97cDo85Juy6RhnouXxi+XpXPdGFwPqVm1glFYos8"
+            + "5+Scbwwx69RihN1nAgMBAAGjYzBhMB0GA1UdDgQWBBRoosUAVtfHHeMl8/5x1m2m"
+            + "arEOoTAfBgNVHSMEGDAWgBRoosUAVtfHHeMl8/5x1m2marEOoTAPBgNVHRMBAf8E"
+            + "BTADAQH/MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAYbLVQlsi"
+            + "KdzCPsaybSrUtMrmkad8Gy+/QR3J16adxy/2WpxsZ1Su0YA4tFzOSWqt74C0mnUi"
+            + "i+3prW7nyEOwezs+NH0SreF4B7tO1FSj157LoHxR2WmCCwul8FfsMi0x6MvEf40v"
+            + "bMeLrRAA4ysRITua1INb05fzJpczoaW/Q1lBPXConvolnIMvYsLbCZX4/OhQQCLa"
+            + "mJx8mGrt1wcNp7Kvh+3JfuOXw+WGeCuB5sSWBsOUvhfN+8sgyk1Dtq5c7rVKKtqz"
+            + "gqHfCNZ+lYa6Vkii7plIcWYJXXa7DMmozX07mrDqdJZEocv6XCoZAKDlJorB8pQT"
+            + "im47RF60X/FCHSCK2cHbG5M+kF84xwj5fTLztLM1+RlJSJiGk6jhxJRQ+hl0Vkhp"
+            + "+u7UbUDwkUF/CJB3d1Gtfm+QtzFVKe27ClU5YFSKCXRV/K4KnkZqpyG8Py+PUtUf"
+            + "WRahp4hkWFkIoLeTnJwgAFRvp/KCtSW0/trI/vfInzqBk/qWIVhxYB8Qv9DXtKBX"
+            + "3AZ36HM2dxmjef/rpYRphuEN0ZwYdynsGy9dF0SihbR8curSg67sbtYfyw0xURhU"
+            + "Nk99YMy7T0EUYnaki/PIPK/gnJjZTX1FLCyHUR38fDJIWkGB4xr8pSrwmRoPSvNF"
+            + "dFr3YlFWwHWd1gXlwJtzeMSuVgoKVtZGmmk=";
+    private static final String ROOT_CA_DIFFERENT_COMMON_NAME_BASE64 = ""
+            + "MIIJFzCCBP6gAwIBAgIJAMobGgw5LXwqMA0GCSqGSIb3DQEBCwUAMBgxFjAUBgNV"
+            + "BAMMDVdyb25nIFJvb3QgQ0EwHhcNMTgwMTE2MTgzNDA5WhcNMzgwMTExMTgzNDA5"
+            + "WjAYMRYwFAYDVQQDDA1Xcm9uZyBSb290IENBMIIEIjANBgkqhkiG9w0BAQEFAAOC"
+            + "BA8AMIIECgKCBAEJwW/TkzWx4s9ghbugWK+Hu7mi0K0ybuvKrRSOltNA9xDElBtj"
+            + "NxRAXsJs9hN1YOJ3gS47xl5zTejCex24WEq53offIhDi54aKoIGgbUPr43x8X1Hx"
+            + "rBOYCxs2DrdUifHojBwFmeMWWwgh+04XGogkFiGIqE0mE+0XWL12f43keMQqaQk7"
+            + "eleFiFFcyns7Av96nHOEFcVhPtoOD2mdozoL7iGaFD/SrajLNi0CoWasR6du0job"
+            + "jtLMxz6yE1O5gu2rbWP9ne4DdHAH1LN+730Xrq2WBNe/srthLnveE3dNBj/6eQt8"
+            + "XL+gXYtBFi8ocX42/QEfNAvOoMRgFogtwx/eLYJ79vvBmOK0uoUAjWzCobOAf9Ck"
+            + "Hc6tfnUz8HtQIXacHs9rySe7iQEMaTPZDjWMAvRb+q2PjdXAkNbRj3w66pCIKomt"
+            + "OsZ2/tv+mGdVUG8Wu0iNgx/IrfiPLlUtdy1XkJKRaPne6Xea8YUckP7qed4NHg9S"
+            + "SZg3SSFPi39GqdCXBgTRroSurX5Gh/r8OhZLN4EsFhUloeFHL2au51H9VxDKcsq3"
+            + "Lpmy3gvKHpo8wdylPAZlsb7iZedZ5NosSca+K+V2SxNc7GKlw799cl8OyzhvSCUb"
+            + "M/cYPT4fkRkH1yhjM1PjtBl312vEd2svG0+iL4x6qQaAJ3IXIFk7wbaT8m7LOJPq"
+            + "HL7nRVcmeXpSPGPsxVRgCwhIeweCoRMOy6RBtsq9MqDeDE9U03+8pbDKHsotwUho"
+            + "4Sg86BK2SZWyqtebtgsToG/Obye7ivAl4AX1ix6D0ERjzpNFGYAjt9bh4MQ+fGAJ"
+            + "RS9xhICHmZxrIjIivQpO5eR6/BhtVEcxWbL0NqrOw/xgVcioW8NdEIVLL+AySg1v"
+            + "1mip2SaNn5UGyMAoj678dp/qmB2kZltUYoQWZpUcKSu0+n+DLiQTiqZ39Ihpxwby"
+            + "joRv1f19upNMNm/fiRwaDE68ElUho0emtOOC9gSdFoMpOWZmgJSYGUfO9t/rxhYG"
+            + "YjPQry9g93H2v3wuJSCltMe0pkhismduA+dsL2Jw/bjaSwHjJpPHVBZzXi9M2/WV"
+            + "S2/jMkSZpqjKc2URMkV/9PPRAonG1P6X8EgDPyQQkwBwqx+hPH05O1+4vzmldsKD"
+            + "FsoqYqfpHPFQLNLOAxugLTPpbxnHLaAsiGoybwBcAXuxqXCtGKtXiVawz0w6FUSB"
+            + "+Ny/+qZBJivUBGvDB6AdGrE8DeqHXWR+rlhIVTtbv58mtYfpW4tnQHV+Pb6Wygw1"
+            + "6QUDVQsPwMrtoAUiH1jXLgm3//Ey449wPk9Y0YWZSTug97CIhQwuNdJZyCtBVhtk"
+            + "QNb++igwmijjtWDV9A96F6Ns40TWcEhp5FWElwIDAQABo2MwYTAdBgNVHQ4EFgQU"
+            + "13uhh5KLuFIaKZGlNloctDLr7NEwHwYDVR0jBBgwFoAU13uhh5KLuFIaKZGlNloc"
+            + "tDLr7NEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcN"
+            + "AQELBQADggQCAAWZS0RIH4FdQhlUkdJW06cUYYIJ7p8rRI+6XgMkfj7vupjVnfDN"
+            + "3Z3s0yf+dGaGGJbv68W952j33z94FuubsacCixNSaS4bIwLXvcL9kQtI9Qf09Pd6"
+            + "GB102Q6nz/daLVF/5urQPbp4UFkyhcP/d2mg9CmxwBL3Djrf/dKf4EoiV5xmQpUB"
+            + "a6BNBMntnK1VZ3y8YxDF3XnhbjUZAOTLLYe4+isqvXPb9osdmdaZQU3iHQQwGYJN"
+            + "6rTYvHmsdfU5eLYLdWxOOm/5Sz5rWXxa1XqSfQgOIaFYQ1w69Z+3BfNbaBnYck3l"
+            + "xtxGHromigt+2iimXFFML7EHiSznVhHl3SOX0RBLeUvP8oNwwSsaHXuXbceYWvb+"
+            + "ic7FyTN4f3+wRGjN01U3be93dj+qZlvTmCzpOrJeUcym3F94d0tWQvk3kkBp/Egi"
+            + "Dd85vYWCdEeCfODW6sReVdj/IuT5xv1T8kKaNaEjJjAjeX6xjPskw3I2LuPeEyz+"
+            + "26LiOs1hPRHC4CL3JS6LNmRmIYKuy7K0DHwxS6wplDYXXH+a0VuLvQrbsw5zTh3f"
+            + "Xwq0CLGwPPRyMnFk13+PYBa0bmJ1dNu5hUc9biziCJlvcg0c+FzzUYG4poN0R73R"
+            + "XPyFHmpULAHit05dw3QaPZqX1GCeiVxrCl6N6G4/9PsVOvi/WEUasHDkk+R4/r9b"
+            + "RwvQw0PVdDvGndouRcSzHvPEdW9y2TxSDhltvtC2xvp3mGaT0j2+cCRDINLFB6rK"
+            + "v18oLzpzu3HaZ2ptmm4OpeRnCTLa4qheV1rlSWi3mvPoh77glgHEyTHvhiFrlq+6"
+            + "f6oMpkcbp4KtOT/npvB3yY4RZWn+J1cDcOW34ssSN2PDVSx1IsianxtSCXKReYrv"
+            + "kjS4sTQWw0LYG6146g2rz1OKzuT+6YSIPlpw/4/DK0Sz/Q1AY0cuOHhgMSW0cSaE"
+            + "6PQ275hFjJ4zYYEYNOp56nhsvgbLAu1V5rwQpwi2RNo4teFzP/AKyZzNbApfl8Q5"
+            + "PKHHE8+Uk3/oLZ8h12JzceKL5ivXU4i8r9sw+o7b/UReVsbrFDXTuO3sRFyA5kI0"
+            + "aBpYrAyb8xVubbi7gCWhDfJsSKc7CR2N+EUtBrT08TD0AtuxpRLr6Sz2RrTabZNV"
+            + "lQlgWmKalWT/i7gyA/MNxbBK0hyr7Pvl5f7Ud2+muKOfhEXiCzp8yzXU9I8cTz2p"
+            + "K85LYJfRNIO2kPrNLdomPL+J2S298GvX2j08CZR8qBXLOs6XJOvZ1KC8BrJ9M7kG"
+            + "2MHGXeL+9/11khM09dbC+Q0NUKTKOpSU7M7RaON1Dp4RyzdIcdwZ/dFvvxcYbtOP"
+            + "6OAzRvlk6CZWG3obkt3yaB1NhBEw8hiYvX9F";
+    private static final String INTERMEDIATE_CA_1_BASE64 = ""
+            + "MIIHMDCCAxegAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UEAwwVR29v"
+            + "Z2xlIENyeXB0QXV0aFZhdWx0MB4XDTE4MDExMjAwMjM1N1oXDTI4MDExMDAwMjM1"
+            + "N1owLTErMCkGA1UEAwwiR29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0"
+            + "ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALlhoKYuwxaatY9ERDFp"
+            + "iEygSnjy0xzaiF4uCyiTfAuboSi5QwGon3ohf0ufJF02L9lnTMoeBAg+88m8AMgW"
+            + "KFcEupabqlZfA3F/50mMCmnJvBSLXJ+chUdcAVpwcZAsq6ko22ARBxao1wu2qxNe"
+            + "D8eXiiK8DRpTtKy3wOldZJ222v35v9JGOTjORZRrcv7Z8f6I5/cSsTS+WoVk/aBt"
+            + "QqyFkcdw1zqulnlFE2rxNAVgyLlW71WJikYTDtUDeo79LvkDGjVsLo2MfpNxuK+5"
+            + "MuMqzyN5LmzXJmNCEW1O5IIIUAPhgy5s08G+3G644wCEWsAnv2FBWLBn/HmJu6Uq"
+            + "nSM2AaJN56V0tJG/yL2VgTnPJrJypNTKZW3OTCLCaYcTEbKfarxLwVWxvIWSIgkn"
+            + "0q57GYhf7O+x9vvcOUmZwVxZECorIiK4n5AWG/KD3dWI3UGGGpYsDazRngA/bQPu"
+            + "DSzBP9FBVcQt3/DMBG1s6f2Eko5f6aTFcVW9iV7aWLeIq+pQYlbmG42decj+aHLQ"
+            + "COp5KV+Q77y4kFhZQFAQ1mN4crnhuEc1K5SmjAK24zIqWbwM3ly0KSQFc9jAmONg"
+            + "0xu7kAObP3PZk85En12yLLscNmHCWYfOOEvTHf4KX7tjBl4HHp/ur+2Qwgpt9MFB"
+            + "MGqR2cni5OV6gZcRdHaEerjrAgMBAAGjZjBkMB0GA1UdDgQWBBRE9RxHT7U3EP1v"
+            + "djRzNYMrU7EseDAfBgNVHSMEGDAWgBTXe6GHkou4UhopkaU2Why0Muvs0TASBgNV"
+            + "HRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC"
+            + "BAIAAfa7FKkfh4t+G8Sv4n3gUcuEAtmpCsTtHJU67r5eSvR24fDX7sqqSIib9sQ8"
+            + "adA1FtnE3wnC4bEKnIwQLerWTN4i4V9oEKRIT5o5Rr61LyO9R+Prpo6uJVoPujFH"
+            + "GZPdIj/Qs4Xax4a11QD+70+ZmmdL6yfigVDWA4yduFdMg6heHRf3EFCbVBv5qbBS"
+            + "n8+eFKRnBZ/kQdFlYt+G+92Qqyn2uAcER6kZjIfPdnZh44SazLP37W8AkDX30Pmk"
+            + "V0PHVGCDrap44q3maI1m8NAE1jGwwmRzJL953x5XgbVGt0K/3cNoWtKLenwX/G3I"
+            + "akrgvOY5Zl0v3FRDZwGFt9UIBfZDDOGRMXIgIGs/1cvkwWpOT6dyReqDXempiQ1q"
+            + "Yy6J5VsK5WK6gEelUyoACbzgby25V6a79Q1MI7dXmFQfCcX0nAD/AZmM1HkeYgrC"
+            + "uq6fWoPOVMKML2mN90rCzkWxGaLcl5dPfad0O1LrcP48SRE5MXMWyxZZBon+wDIk"
+            + "ascyM/r4fmk4kq64YKdm2wxCDMNArAIcyBkwOaWWfabtSagxJ3qtMtxK0qBUsbLC"
+            + "yMyYpgU1h9c8rEdc4JgeE2LXJzxTKDc3SBOqbuNMlKWjYA+X+SUvVYALrQKAC+5v"
+            + "wdUhLYdAPAksqk/ZoiBjkW35FfvqQMJBY29VnDT1h7/Nxk5gu+goTA9oFIYNrNte"
+            + "+s0my+IUgYhKJBsgh7Mupv+B92GN5b3b440BMHB5QR959Jdq6BAXNUyZLM5fhZQE"
+            + "Jj/rxZFXaqq757kgUhwWBz5TDbYF7GkqTyM4k430xwJKY0AYYEHmv1UYNo5X4G3x"
+            + "SC2LhWC1b9VAykdkHbLs+IA8klxURmLmRiRj1UryhQjjT8h/FvNyPnbT1AKoElix"
+            + "QLnLi8thkJ+tQggO0hISFsIrKNfnn0V6O0VKw9UZsMigsbYG5EbzIXcAyy8Avr9n"
+            + "um7gBBZDt7fWso0+pG1UenJ+PybeuW/azQDLRw1Syz8OwU+ABRLq0JyyAtV7VPY5"
+            + "C9pkKS+bU8nECxr6dMhAbpLBHlKsyb1qtkOt1p7WagEQZFIIc6svc73+L/ET/lWn"
+            + "GBmkVVsCN7Aqyo5aXQWueXP4FUL+6O5+JALqw3qPeQgfnLkh0cUuccNND05QeEiv"
+            + "Zswc/23KJXy1XbdVKT3UP0RAF7DxstbRGQbAT3z+n931e3KhtU28OKjsFtoeq2Dj"
+            + "6STPEXh4rYFWMM8+DrJetAtBqk/i+vBwRA8f7jqIPPep/vEjPqqMOpdSVcoFQ1df"
+            + "JuOZtGfEUjFHnlDr3eGP7KUIEZvhan1zm544dDgPVTXxrY4moi2BhKEY69zRSX6B"
+            + "+a0fa5B3pxc8BN0LsHA0stT/Y2o=";
+    private static final String INTERMEDIATE_CA_2_BASE64 = ""
+            + "MIIESTCCAjGgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwiR29v"
+            + "Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTIwMDM4MDNa"
+            + "Fw0yMzAxMTEwMDM4MDNaMDoxODA2BgNVBAMML0dvb2dsZSBDcnlwdEF1dGhWYXVs"
+            + "dCBJbnRlcm1lZGlhdGUgSW50ZXJtZWRpYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOC"
+            + "AQ8AMIIBCgKCAQEA0v3bN3MwKifDAkF1524XzuaxYtk1sQKUlAlNngh+Qv4RjCkX"
+            + "TECi7po8LeNsY+hWxmW3XZ22RBphe/yP4YcOdbqlIjZYNx3b75hCSJCadOkdW+Z9"
+            + "f6+tKsHgeUja6r9r2TThzileImAvjXShe7GZYW8csPv6HaEVRXQlu8fGAZf8skmJ"
+            + "EMfJx84//WeULdVz94beDhi9YAf4gLfmOayQcdWhDcMYI39knJcRny1ffRGgb1Hf"
+            + "lE+3/a3aGFeODaxfkPaGRxEhzhZ/JDBiNgUAH/u7C5nxqa2WOu5e0wq3S0TndIOE"
+            + "hmnwCE2GvxADFQst+rSsOn4EHit70hv4CfrMRQIDAQABo2YwZDAdBgNVHQ4EFgQU"
+            + "0dKv4xTaEmdwHyox3tY8H8XVDGIwHwYDVR0jBBgwFoAURPUcR0+1NxD9b3Y0czWD"
+            + "K1OxLHgwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZI"
+            + "hvcNAQELBQADggIBAJaArqLlJ2SLQ8JRwanD6LPlqQxucQ+x/LztTQpzPrsFfJds"
+            + "E/sDZr2rXhDpz/bifIdj/DCbQ6/3w+t5JdFjT8GjXLgz1kCa/4W409FgSTgy1ENn"
+            + "AMUU6pFIbOq/Qy/71uOTrrFWYTN5Vk+RBGxx5iDfHjDYraudi4VlcNkal4OyM98n"
+            + "N3qp9cZD0RtWxMhvq6ahgmf9cTbEw6+l8yf/bogGLBYXXYeOoO5Q134AxrrgfthE"
+            + "tvyKwJkT/l3OFKRcaHrebs+V1z5gPs7zWOyO5n2Z1SAmcOGfTfKMZWwp3Hi3OTr2"
+            + "gB3LUYKyQVhC70dka3X+IbnFg5YfzJtX6YGnHlnI1SufOkEpGQDfcc0UQAWg/lgb"
+            + "RkfMFD9tuJomBhyqv1YaxLN8yL4ZTRU0KCvvC5I5+X/zt9kBjnHlBOdYtknZT5jz"
+            + "7+mjqWdpmWoAjeV5+CgIzG2k7JAm6rQuE1ZQNF0wAYxPret4NHPJFqfD5gGhdrYw"
+            + "pEUxkcwHERA/E1CkpyqUy/Hd3kqHvnEDqzFcxBdUdmOgnbpI2nAZdEpfxmA5+M1n"
+            + "UoxQ8ZWAZH+Mdlkw/Hx5hVjGjz8snD4QN25pj/wT+V6AR5OmYb8yfsQb2S8a8yDp"
+            + "HzcIHW+dEWpX2boirOsrdI16kNtxYqtG7c5qWBPJy5Zjkvh9qbnfT/RQx10g";
+    private static final String LEAF_CERT_1_BASE64 = ""
+            + "MIIDCDCB8aADAgECAgYBYOlweDswDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAwwi"
+            + "R29vZ2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZTAeFw0xODAxMTEwODE1"
+            + "NTBaFw0yMDAxMTIwODE1NTBaMCkxJzAlBgNVBAMTHkdvb2dsZSBDcnlwdEF1dGhW"
+            + "YXVsdCBJbnN0YW5jZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABLgAERiYHfBu"
+            + "tJT+htocB40BtDr2jdxh0EZJlQ8QhpMkZuA/0t/zeSAdkVWw5b16izJ9JVOi/KVl"
+            + "4b0hRH54UvowDQYJKoZIhvcNAQELBQADggIBABZALhC9j3hpZ0AgN0tsqAP2Ix21"
+            + "tNOcvo/aFJuSFanOM4DZbycZEYAo5rorvuFu7eXETBKDGnI5xreNAoQsaj/dyCHu"
+            + "HKIn5P7yCmKvG2sV2TQ5go+0xV2x8BhTrtUWLeHvUbM3fXipa3NrordbA8MgzXwr"
+            + "GR1Y1FuMOn5n4kiuHJ2sQTbDdzSQSK5VpH+6rjARlfOCyLUX0u8UKRRH81qhIQWb"
+            + "UFMp9q1CVfiLP2O3CdDdpZXCysdflIb62TWnma+I8jqMryyxrMVs9kpfa8zkX9qe"
+            + "33Vxp+QaQTqQ07/7KYVw869MeFn+bXeHnjUhqGY6S8M71vrTMG3M5p8Sq9LmV8Y5"
+            + "7YB5uqKap2Inf0FOuJS7h7nVVzU/kOFkepaQVHyScwTPuuXNgpQg8XZnN/AWfRwJ"
+            + "hf5zE6vXXTHMzQA1mY2eEhxGfpryv7LH8pvfcyTakdBlw8aMJjKdre8xLLGZeVCa"
+            + "79plkfYD0rMrxtRHCGyTKGzUcx/B9kYJK5qBgJiDJLKF3XwGbAs/F8CyEPihjvj4"
+            + "M2EoeyhmHWKLYsps6+uTksJ+PxZU14M7672K2y8BdulyfkZIhili118XnRykKkMf"
+            + "JLQJKMqZx5O0B9bF8yQdcGKEGEwMQt5ENdH8HeiwLm4QS3VzFXYetgUPCM5lPDIp"
+            + "BuwwuQxvQDF4pmQd";
+    private static final String LEAF_CERT_2_BASE64 = ""
+            + "MIICrDCCAZSgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOjE4MDYGA1UEAwwvR29v"
+            + "Z2xlIENyeXB0QXV0aFZhdWx0IEludGVybWVkaWF0ZSBJbnRlcm1lZGlhdGUwHhcN"
+            + "MTgwMTEyMDEwMzA5WhcNMTkwMTEyMDEwMzA5WjArMSkwJwYDVQQDDCBHb29nbGUg"
+            + "Q3J5cHRBdXRoVmF1bHQgSW5zdGFuY2UgMjBZMBMGByqGSM49AgEGCCqGSM49AwEH"
+            + "A0IABGhmBQyWdjsXKJRbkW4iIrvt6iqhX5t2XGt/vZS9CoOl0fs+EvJXo4kgrnx8"
+            + "/8SGxz3pwRkFhY943QYy6a1gv/2jgZUwgZIwCQYDVR0TBAIwADAdBgNVHQ4EFgQU"
+            + "xFmLyxUS2JHKURBtewBKRP6kQBgwVgYDVR0jBE8wTYAU0dKv4xTaEmdwHyox3tY8"
+            + "H8XVDGKhMaQvMC0xKzApBgNVBAMMIkdvb2dsZSBDcnlwdEF1dGhWYXVsdCBJbnRl"
+            + "cm1lZGlhdGWCAhAAMA4GA1UdDwEB/wQEAwIDCDANBgkqhkiG9w0BAQsFAAOCAQEA"
+            + "EJWpl7HU6LxukLqhw2tVZr7IRrKIucRk+RKaaiMx1Hx2jsTTskiJRiZas/xoPSqX"
+            + "z1K5DVgI486i7HyqnWkGH5xVzCsv+rya5FOSTS3jVtgtoA4HFEqeeAcDowPDqVw3"
+            + "yFTA55ukZnzVaPLpDfPqkhzWiuLQ/4fI6YCmOnWB8KtHTMdyGsDSAkpoxpok++NJ"
+            + "Lu79BoBLe2ucjN383lTlieLxmrmHjF9ryYSQczcm0v6irMOMxEovw5iT4LHiEhbm"
+            + "DfOPW909fe/s+K3TGZ3Q6U77x8g5k9dVovMgA4pFwtREtknFjeK1wXR3/eXGcP3W"
+            + "0bMX1yTWYJQFWCG3DFoC5w==";
+
+    /** The cert of the root CA. */
+    static final X509Certificate ROOT_CA_TRUSTED = decodeBase64Cert(ROOT_CA_TRUSTED_BASE64);
+    /** This root CA cert has a different Common Name than ROOT_CA_TRUSTED. */
+    static final X509Certificate ROOT_CA_DIFFERENT_COMMON_NAME =
+            decodeBase64Cert(ROOT_CA_DIFFERENT_COMMON_NAME_BASE64);
+    /** This root CA cert has the same CN as ROOT_CA_TRUSTED, but a different public key. */
+    static final X509Certificate ROOT_CA_DIFFERENT_KEY =
+            decodeBase64Cert(ROOT_CA_DIFFERENT_KEY_BASE64);
+    /** This intermediate CA cert is signed by the corresponding private key of ROOT_CA_TRUSTED. */
+    static final X509Certificate INTERMEDIATE_CA_1 = decodeBase64Cert(INTERMEDIATE_CA_1_BASE64);
+    /** This intermediate CA cert is signed by the private key of INTERMEDIATE_CA_1. */
+    static final X509Certificate INTERMEDIATE_CA_2 = decodeBase64Cert(INTERMEDIATE_CA_2_BASE64);
+    /** This leaf cert is signed by the corresponding private key of INTERMEDIATE_CA_1. */
+    static final X509Certificate LEAF_CERT_1 = decodeBase64Cert(LEAF_CERT_1_BASE64);
+    /** This leaf cert is signed by the corresponding private key of INTERMEDIATE_CA_2. */
+    static final X509Certificate LEAF_CERT_2 = decodeBase64Cert(LEAF_CERT_2_BASE64);
+
+    private static X509Certificate decodeBase64Cert(String str) {
+        try {
+            byte[] bytes = Base64.getDecoder().decode(str);
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(bytes));
+        } catch (Exception e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    static final Date DATE_ALL_CERTS_VALID = new Date(1516406400000L); // Jan 20, 2018
+    static final Date DATE_LEAF_CERT_2_EXPIRED = new Date(1547254989001L); // Jan 12, 2019
+    static final Date DATE_INTERMEDIATE_CA_2_EXPIRED = new Date(1673397483001L); // Jan 11, 2023
+
+    static InputStream openTestFile(String relativePath) throws Exception {
+        Context context = InstrumentationRegistry.getContext();
+        return context.getResources().getAssets().open(TEST_FILE_FOLDER_NAME + "/" + relativePath);
+    }
+
+    static byte[] readTestFile(String relativePath) throws Exception {
+        InputStream in = openTestFile(relativePath);
+        return ByteStreams.toByteArray(in);
+    }
+}
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 63f970a..e0b0bbf 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -787,6 +787,10 @@
             builder.append(isLong ? " PROPERTY_HAS_CDMA_VOICE_PRIVACY" : " priv");
         }
 
+        if (can(properties, PROPERTY_IS_RTT)) {
+            builder.append(isLong ? " PROPERTY_IS_RTT" : " rtt");
+        }
+
         builder.append("]");
         return builder.toString();
     }
@@ -2646,6 +2650,7 @@
      * side of the coll.
      */
     public final void sendRttSessionRemotelyTerminated() {
+        unsetRttProperty();
         mListeners.forEach((l) -> l.onRttSessionRemotelyTerminated(Connection.this));
     }
 
diff --git a/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java b/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
index 1e8382f..e8e4dc2 100644
--- a/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
+++ b/wifi/tests/src/android/net/wifi/p2p/WifiP2pManagerTest.java
@@ -59,10 +59,10 @@
      */
     @Test
     public void testChannelFinalize() throws Exception {
-        WifiP2pManager.Channel channel = new WifiP2pManager.Channel(mContextMock,
-                mTestLooper.getLooper(), null, null, mDut);
-
-        leakageDetectorRule.assertUnreleasedResourceCount(channel, 1);
+        try (WifiP2pManager.Channel channel = new WifiP2pManager.Channel(mContextMock,
+                mTestLooper.getLooper(), null, null, mDut)) {
+            leakageDetectorRule.assertUnreleasedResourceCount(channel, 1);
+        }
     }
 
     /**