Merge "Use sqlite with no ICU support"
diff --git a/identity/TEST_MAPPING b/identity/TEST_MAPPING
index 87707a8..6444c56 100644
--- a/identity/TEST_MAPPING
+++ b/identity/TEST_MAPPING
@@ -2,6 +2,9 @@
   "presubmit": [
     {
       "name": "CtsIdentityTestCases"
+    },
+    {
+      "name": "identity-credential-util-tests"
     }
   ]
 }
diff --git a/identity/util/Android.bp b/identity/util/Android.bp
new file mode 100644
index 0000000..71d7718
--- /dev/null
+++ b/identity/util/Android.bp
@@ -0,0 +1,42 @@
+// Copyright 2021 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+    name: "identity-credential-util",
+    srcs: [
+        "src/java/**/*.java",
+    ],
+    static_libs: [
+        "androidx.annotation_annotation",
+        "bouncycastle-unbundled",
+        "cbor-java",
+    ],
+}
+
+android_test {
+    name: "identity-credential-util-tests",
+    test_suites: ["general-tests"],
+    srcs: [
+        "test/java/**/*.java",
+    ],
+    static_libs: [
+        "androidx.test.rules",
+        "identity-credential-util",
+        "junit",
+    ],
+}
diff --git a/identity/util/AndroidManifest.xml b/identity/util/AndroidManifest.xml
new file mode 100644
index 0000000..eece4dc
--- /dev/null
+++ b/identity/util/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright 2021 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.security.identity.internal">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.security.identity.internal"
+        android:label="Unit tests for com.android.security.identity.internal"/>
+
+</manifest>
+
diff --git a/identity/util/AndroidTest.xml b/identity/util/AndroidTest.xml
new file mode 100644
index 0000000..345460f
--- /dev/null
+++ b/identity/util/AndroidTest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 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.
+-->
+<configuration description="Config for identity cred support library tests">
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="identity-credential-util-tests.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.InstrumentationTest" >
+        <option name="package" value="com.android.security.identity.internal" />
+    </test>
+</configuration>
diff --git a/identity/util/src/java/com/android/security/identity/internal/Iso18013.java b/identity/util/src/java/com/android/security/identity/internal/Iso18013.java
new file mode 100644
index 0000000..6da90e5
--- /dev/null
+++ b/identity/util/src/java/com/android/security/identity/internal/Iso18013.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2021 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.security.identity.internal;
+
+import static com.android.security.identity.internal.Util.CBOR_SEMANTIC_TAG_ENCODED_CBOR;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.InvalidParameterException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECPoint;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.crypto.KeyAgreement;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import co.nstant.in.cbor.CborBuilder;
+import co.nstant.in.cbor.CborDecoder;
+import co.nstant.in.cbor.CborEncoder;
+import co.nstant.in.cbor.CborException;
+import co.nstant.in.cbor.builder.MapBuilder;
+import co.nstant.in.cbor.model.ByteString;
+import co.nstant.in.cbor.model.DataItem;
+
+/**
+ * Various utilities for working with the ISO mobile driving license (mDL)
+ * application specification (ISO 18013-5).
+ */
+public class Iso18013 {
+    /**
+     * Each version of the spec is namespaced, and all namespace-specific constants
+     * are thus collected into a namespace-specific nested class.
+     */
+    public static class V1 {
+        public static final String NAMESPACE = "org.iso.18013.5.1";
+        public static final String DOC_TYPE = "org.iso.18013.5.1.mdl";
+
+        public static final String FAMILY_NAME = "family_name";
+        public static final String GIVEN_NAME = "given_name";
+        public static final String BIRTH_DATE = "birth_date";
+        public static final String ISSUE_DATE = "issue_date";
+        public static final String EXPIRY = "expiry_date";
+        public static final String ISSUING_COUNTRY = "issuing_country";
+        public static final String ISSUING_AUTHORITY = "issuing_authority";
+        public static final String DOCUMENT_NUMBER = "document_number";
+        public static final String PORTRAIT = "portrait";
+        public static final String DRIVING_PRIVILEGES = "driving_privileges";
+        public static final String UN_DISTINGUISHING_SIGN = "un_distinguishing_sign";
+        public static final String HEIGHT = "height";
+        public static final String BIO_FACE = "biometric_template_face";
+
+        public static String ageOver(int age) {
+            if (age < 0 || age > 99) {
+                throw new InvalidParameterException("age must be between 0 and 99, inclusive");
+            }
+            return String.format("age_over_%02d", age);
+        }
+    }
+
+    public static byte[] buildDeviceAuthenticationCbor(String docType,
+            byte[] encodedSessionTranscript,
+            byte[] deviceNameSpacesBytes) {
+        ByteArrayOutputStream daBaos = new ByteArrayOutputStream();
+        try {
+            ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript);
+            List<DataItem> dataItems = null;
+            dataItems = new CborDecoder(bais).decode();
+            DataItem sessionTranscript = dataItems.get(0);
+            ByteString deviceNameSpacesBytesItem = new ByteString(deviceNameSpacesBytes);
+            deviceNameSpacesBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+            new CborEncoder(daBaos).encode(new CborBuilder()
+                    .addArray()
+                    .add("DeviceAuthentication")
+                    .add(sessionTranscript)
+                    .add(docType)
+                    .add(deviceNameSpacesBytesItem)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding DeviceAuthentication", e);
+        }
+        return daBaos.toByteArray();
+    }
+
+    public static byte[] buildReaderAuthenticationBytesCbor(
+            byte[] encodedSessionTranscript,
+            byte[] requestMessageBytes) {
+
+        ByteArrayOutputStream daBaos = new ByteArrayOutputStream();
+        try {
+            ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript);
+            List<DataItem> dataItems = null;
+            dataItems = new CborDecoder(bais).decode();
+            DataItem sessionTranscript = dataItems.get(0);
+            ByteString requestMessageBytesItem = new ByteString(requestMessageBytes);
+            requestMessageBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+            new CborEncoder(daBaos).encode(new CborBuilder()
+                    .addArray()
+                    .add("ReaderAuthentication")
+                    .add(sessionTranscript)
+                    .add(requestMessageBytesItem)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding ReaderAuthentication", e);
+        }
+        byte[] readerAuthentication = daBaos.toByteArray();
+        return Util.prependSemanticTagForEncodedCbor(readerAuthentication);
+    }
+
+    // This returns a SessionTranscript which satisfy the requirement
+    // that the uncompressed X and Y coordinates of the public key for the
+    // mDL's ephemeral key-pair appear somewhere in the encoded
+    // DeviceEngagement.
+    public static byte[] buildSessionTranscript(KeyPair ephemeralKeyPair) {
+        // Make the coordinates appear in an already encoded bstr - this
+        // mimics how the mDL COSE_Key appear as encoded data inside the
+        // encoded DeviceEngagement
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            ECPoint w = ((ECPublicKey) ephemeralKeyPair.getPublic()).getW();
+            // X and Y are always positive so for interop we remove any leading zeroes
+            // inserted by the BigInteger encoder.
+            byte[] x = stripLeadingZeroes(w.getAffineX().toByteArray());
+            byte[] y = stripLeadingZeroes(w.getAffineY().toByteArray());
+            baos.write(new byte[]{41});
+            baos.write(x);
+            baos.write(y);
+            baos.write(new byte[]{42, 44});
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+        byte[] blobWithCoords = baos.toByteArray();
+
+        baos = new ByteArrayOutputStream();
+        try {
+            new CborEncoder(baos).encode(new CborBuilder()
+                    .addArray()
+                    .add(blobWithCoords)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            e.printStackTrace();
+            return null;
+        }
+        ByteString encodedDeviceEngagementItem = new ByteString(baos.toByteArray());
+        ByteString encodedEReaderKeyItem = new ByteString(Util.cborEncodeString("doesn't matter"));
+        encodedDeviceEngagementItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+        encodedEReaderKeyItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+
+        baos = new ByteArrayOutputStream();
+        try {
+            new CborEncoder(baos).encode(new CborBuilder()
+                    .addArray()
+                    .add(encodedDeviceEngagementItem)
+                    .add(encodedEReaderKeyItem)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            e.printStackTrace();
+            return null;
+        }
+        return baos.toByteArray();
+    }
+
+    /*
+     * Helper function to create a CBOR data for requesting data items. The IntentToRetain
+     * value will be set to false for all elements.
+     *
+     * <p>The returned CBOR data conforms to the following CDDL schema:</p>
+     *
+     * <pre>
+     *   ItemsRequest = {
+     *     ? "docType" : DocType,
+     *     "nameSpaces" : NameSpaces,
+     *     ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide
+     *   }
+     *
+     *   NameSpaces = {
+     *     + NameSpace => DataElements     ; Requested data elements for each NameSpace
+     *   }
+     *
+     *   DataElements = {
+     *     + DataElement => IntentToRetain
+     *   }
+     *
+     *   DocType = tstr
+     *
+     *   DataElement = tstr
+     *   IntentToRetain = bool
+     *   NameSpace = tstr
+     * </pre>
+     *
+     * @param entriesToRequest       The entries to request, organized as a map of namespace
+     *                               names with each value being a collection of data elements
+     *                               in the given namespace.
+     * @param docType                  The document type or {@code null} if there is no document
+     *                                 type.
+     * @return CBOR data conforming to the CDDL mentioned above.
+     */
+    public static @NonNull
+    byte[] createItemsRequest(
+            @NonNull Map<String, Collection<String>> entriesToRequest,
+            @Nullable String docType) {
+        CborBuilder builder = new CborBuilder();
+        MapBuilder<CborBuilder> mapBuilder = builder.addMap();
+        if (docType != null) {
+            mapBuilder.put("docType", docType);
+        }
+
+        MapBuilder<MapBuilder<CborBuilder>> nsMapBuilder = mapBuilder.putMap("nameSpaces");
+        for (String namespaceName : entriesToRequest.keySet()) {
+            Collection<String> entryNames = entriesToRequest.get(namespaceName);
+            MapBuilder<MapBuilder<MapBuilder<CborBuilder>>> entryNameMapBuilder =
+                    nsMapBuilder.putMap(namespaceName);
+            for (String entryName : entryNames) {
+                entryNameMapBuilder.put(entryName, false);
+            }
+        }
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        CborEncoder encoder = new CborEncoder(baos);
+        try {
+            encoder.encode(builder.build());
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding CBOR", e);
+        }
+        return baos.toByteArray();
+    }
+
+    public static SecretKey calcEMacKeyForReader(PublicKey authenticationPublicKey,
+            PrivateKey ephemeralReaderPrivateKey,
+            byte[] encodedSessionTranscript) {
+        try {
+            KeyAgreement ka = KeyAgreement.getInstance("ECDH");
+            ka.init(ephemeralReaderPrivateKey);
+            ka.doPhase(authenticationPublicKey, true);
+            byte[] sharedSecret = ka.generateSecret();
+
+            byte[] sessionTranscriptBytes =
+                    Util.cborEncode(Util.buildCborTaggedByteString(encodedSessionTranscript));
+
+            byte[] salt = MessageDigest.getInstance("SHA-256").digest(sessionTranscriptBytes);
+            byte[] info = new byte[]{'E', 'M', 'a', 'c', 'K', 'e', 'y'};
+            byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
+
+            SecretKey secretKey = new SecretKeySpec(derivedKey, "");
+            return secretKey;
+        } catch (InvalidKeyException
+                | NoSuchAlgorithmException e) {
+            throw new IllegalStateException("Error performing key agreement", e);
+        }
+    }
+
+    private static byte[] stripLeadingZeroes(byte[] value) {
+        int n = 0;
+        while (n < value.length && value[n] == 0) {
+            n++;
+        }
+        int newLen = value.length - n;
+        byte[] ret = new byte[newLen];
+        int m = 0;
+        while (n < value.length) {
+            ret[m++] = value[n++];
+        }
+        return ret;
+    }
+}
diff --git a/identity/util/src/java/com/android/security/identity/internal/Util.java b/identity/util/src/java/com/android/security/identity/internal/Util.java
new file mode 100644
index 0000000..b74efb7
--- /dev/null
+++ b/identity/util/src/java/com/android/security/identity/internal/Util.java
@@ -0,0 +1,1314 @@
+/*
+ * Copyright 2019 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.security.identity.internal;
+
+import android.security.identity.ResultData;
+import android.security.identity.IdentityCredentialStore;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.FeatureInfo;
+import android.os.SystemProperties;
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyStore;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.ECGenParameterSpec;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Formatter;
+import java.util.Map;
+
+import javax.crypto.KeyAgreement;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import java.security.interfaces.ECPublicKey;
+import java.security.spec.ECPoint;
+
+import org.bouncycastle.asn1.ASN1InputStream;
+import org.bouncycastle.asn1.ASN1OctetString;
+
+import co.nstant.in.cbor.CborBuilder;
+import co.nstant.in.cbor.CborDecoder;
+import co.nstant.in.cbor.CborEncoder;
+import co.nstant.in.cbor.CborException;
+import co.nstant.in.cbor.builder.ArrayBuilder;
+import co.nstant.in.cbor.builder.MapBuilder;
+import co.nstant.in.cbor.model.AbstractFloat;
+import co.nstant.in.cbor.model.Array;
+import co.nstant.in.cbor.model.ByteString;
+import co.nstant.in.cbor.model.DataItem;
+import co.nstant.in.cbor.model.DoublePrecisionFloat;
+import co.nstant.in.cbor.model.MajorType;
+import co.nstant.in.cbor.model.NegativeInteger;
+import co.nstant.in.cbor.model.SimpleValue;
+import co.nstant.in.cbor.model.SimpleValueType;
+import co.nstant.in.cbor.model.SpecialType;
+import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
+
+public class Util {
+    private static final String TAG = "Util";
+
+    public static byte[] canonicalizeCbor(byte[] encodedCbor) throws CborException {
+        ByteArrayInputStream bais = new ByteArrayInputStream(encodedCbor);
+        List<DataItem> dataItems = new CborDecoder(bais).decode();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        for(DataItem dataItem : dataItems) {
+            CborEncoder encoder = new CborEncoder(baos);
+            encoder.encode(dataItem);
+        }
+        return baos.toByteArray();
+    }
+
+
+    public static String cborPrettyPrint(byte[] encodedBytes) throws CborException {
+        StringBuilder sb = new StringBuilder();
+
+        ByteArrayInputStream bais = new ByteArrayInputStream(encodedBytes);
+        List<DataItem> dataItems = new CborDecoder(bais).decode();
+        int count = 0;
+        for (DataItem dataItem : dataItems) {
+            if (count > 0) {
+                sb.append(",\n");
+            }
+            cborPrettyPrintDataItem(sb, 0, dataItem);
+            count++;
+        }
+
+        return sb.toString();
+    }
+
+    // Returns true iff all elements in |items| are not compound (e.g. an array or a map).
+    static boolean cborAreAllDataItemsNonCompound(List<DataItem> items) {
+        for (DataItem item : items) {
+            switch (item.getMajorType()) {
+                case ARRAY:
+                case MAP:
+                    return false;
+                default:
+                    // continue inspecting other data items
+            }
+        }
+        return true;
+    }
+
+    public static void cborPrettyPrintDataItem(StringBuilder sb, int indent, DataItem dataItem) {
+        StringBuilder indentBuilder = new StringBuilder();
+        for (int n = 0; n < indent; n++) {
+            indentBuilder.append(' ');
+        }
+        String indentString = indentBuilder.toString();
+
+        if (dataItem.hasTag()) {
+            sb.append(String.format("tag %d ", dataItem.getTag().getValue()));
+        }
+
+        switch (dataItem.getMajorType()) {
+            case INVALID:
+                // TODO: throw
+                sb.append("<invalid>");
+                break;
+            case UNSIGNED_INTEGER: {
+                // Major type 0: an unsigned integer.
+                BigInteger value = ((UnsignedInteger) dataItem).getValue();
+                sb.append(value);
+            }
+            break;
+            case NEGATIVE_INTEGER: {
+                // Major type 1: a negative integer.
+                BigInteger value = ((NegativeInteger) dataItem).getValue();
+                sb.append(value);
+            }
+            break;
+            case BYTE_STRING: {
+                // Major type 2: a byte string.
+                byte[] value = ((ByteString) dataItem).getBytes();
+                sb.append("[");
+                int count = 0;
+                for (byte b : value) {
+                    if (count > 0) {
+                        sb.append(", ");
+                    }
+                    sb.append(String.format("0x%02x", b));
+                    count++;
+                }
+                sb.append("]");
+            }
+            break;
+            case UNICODE_STRING: {
+                // Major type 3: string of Unicode characters that is encoded as UTF-8 [RFC3629].
+                String value = ((UnicodeString) dataItem).getString();
+                // TODO: escape ' in |value|
+                sb.append("'" + value + "'");
+            }
+            break;
+            case ARRAY: {
+                // Major type 4: an array of data items.
+                List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems();
+                if (items.size() == 0) {
+                    sb.append("[]");
+                } else if (cborAreAllDataItemsNonCompound(items)) {
+                    // The case where everything fits on one line.
+                    sb.append("[");
+                    int count = 0;
+                    for (DataItem item : items) {
+                        cborPrettyPrintDataItem(sb, indent, item);
+                        if (++count < items.size()) {
+                            sb.append(", ");
+                        }
+                    }
+                    sb.append("]");
+                } else {
+                    sb.append("[\n" + indentString);
+                    int count = 0;
+                    for (DataItem item : items) {
+                        sb.append("  ");
+                        cborPrettyPrintDataItem(sb, indent + 2, item);
+                        if (++count < items.size()) {
+                            sb.append(",");
+                        }
+                        sb.append("\n" + indentString);
+                    }
+                    sb.append("]");
+                }
+            }
+            break;
+            case MAP: {
+                // Major type 5: a map of pairs of data items.
+                Collection<DataItem> keys = ((co.nstant.in.cbor.model.Map) dataItem).getKeys();
+                if (keys.size() == 0) {
+                    sb.append("{}");
+                } else {
+                    sb.append("{\n" + indentString);
+                    int count = 0;
+                    for (DataItem key : keys) {
+                        sb.append("  ");
+                        DataItem value = ((co.nstant.in.cbor.model.Map) dataItem).get(key);
+                        cborPrettyPrintDataItem(sb, indent + 2, key);
+                        sb.append(" : ");
+                        cborPrettyPrintDataItem(sb, indent + 2, value);
+                        if (++count < keys.size()) {
+                            sb.append(",");
+                        }
+                        sb.append("\n" + indentString);
+                    }
+                    sb.append("}");
+                }
+            }
+            break;
+            case TAG:
+                // Major type 6: optional semantic tagging of other major types
+                //
+                // We never encounter this one since it's automatically handled via the
+                // DataItem that is tagged.
+                throw new RuntimeException("Semantic tag data item not expected");
+
+            case SPECIAL:
+                // Major type 7: floating point numbers and simple data types that need no
+                // content, as well as the "break" stop code.
+                if (dataItem instanceof SimpleValue) {
+                    switch (((SimpleValue) dataItem).getSimpleValueType()) {
+                        case FALSE:
+                            sb.append("false");
+                            break;
+                        case TRUE:
+                            sb.append("true");
+                            break;
+                        case NULL:
+                            sb.append("null");
+                            break;
+                        case UNDEFINED:
+                            sb.append("undefined");
+                            break;
+                        case RESERVED:
+                            sb.append("reserved");
+                            break;
+                        case UNALLOCATED:
+                            sb.append("unallocated");
+                            break;
+                    }
+                } else if (dataItem instanceof DoublePrecisionFloat) {
+                    DecimalFormat df = new DecimalFormat("0",
+                            DecimalFormatSymbols.getInstance(Locale.ENGLISH));
+                    df.setMaximumFractionDigits(340);
+                    sb.append(df.format(((DoublePrecisionFloat) dataItem).getValue()));
+                } else if (dataItem instanceof AbstractFloat) {
+                    DecimalFormat df = new DecimalFormat("0",
+                            DecimalFormatSymbols.getInstance(Locale.ENGLISH));
+                    df.setMaximumFractionDigits(340);
+                    sb.append(df.format(((AbstractFloat) dataItem).getValue()));
+                } else {
+                    sb.append("break");
+                }
+                break;
+        }
+    }
+
+    public static byte[] encodeCbor(List<DataItem> dataItems) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        CborEncoder encoder = new CborEncoder(baos);
+        try {
+            encoder.encode(dataItems);
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding data", e);
+        }
+        return baos.toByteArray();
+    }
+
+    public static byte[] coseBuildToBeSigned(byte[] encodedProtectedHeaders,
+            byte[] payload,
+            byte[] detachedContent) {
+        CborBuilder sigStructure = new CborBuilder();
+        ArrayBuilder<CborBuilder> array = sigStructure.addArray();
+
+        array.add("Signature1");
+        array.add(encodedProtectedHeaders);
+
+        // We currently don't support Externally Supplied Data (RFC 8152 section 4.3)
+        // so external_aad is the empty bstr
+        byte emptyExternalAad[] = new byte[0];
+        array.add(emptyExternalAad);
+
+        // Next field is the payload, independently of how it's transported (RFC
+        // 8152 section 4.4). Since our API specifies only one of |data| and
+        // |detachedContent| can be non-empty, it's simply just the non-empty one.
+        if (payload != null && payload.length > 0) {
+            array.add(payload);
+        } else {
+            array.add(detachedContent);
+        }
+        array.end();
+        return encodeCbor(sigStructure.build());
+    }
+
+    private static final int COSE_LABEL_ALG = 1;
+    private static final int COSE_LABEL_X5CHAIN = 33;  // temporary identifier
+
+    // From "COSE Algorithms" registry
+    private static final int COSE_ALG_ECDSA_256 = -7;
+    private static final int COSE_ALG_HMAC_256_256 = 5;
+
+    private static byte[] signatureDerToCose(byte[] signature) {
+        if (signature.length > 128) {
+            throw new RuntimeException("Unexpected length " + signature.length
+                    + ", expected less than 128");
+        }
+        if (signature[0] != 0x30) {
+            throw new RuntimeException("Unexpected first byte " + signature[0]
+                    + ", expected 0x30");
+        }
+        if ((signature[1] & 0x80) != 0x00) {
+            throw new RuntimeException("Unexpected second byte " + signature[1]
+                    + ", bit 7 shouldn't be set");
+        }
+        int rOffset = 2;
+        int rSize = signature[rOffset + 1];
+        byte[] rBytes = stripLeadingZeroes(
+            Arrays.copyOfRange(signature,rOffset + 2, rOffset + rSize + 2));
+
+        int sOffset = rOffset + 2 + rSize;
+        int sSize = signature[sOffset + 1];
+        byte[] sBytes = stripLeadingZeroes(
+            Arrays.copyOfRange(signature, sOffset + 2, sOffset + sSize + 2));
+
+        if (rBytes.length > 32) {
+            throw new RuntimeException("rBytes.length is " + rBytes.length + " which is > 32");
+        }
+        if (sBytes.length > 32) {
+            throw new RuntimeException("sBytes.length is " + sBytes.length + " which is > 32");
+        }
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            for (int n = 0; n < 32 - rBytes.length; n++) {
+                baos.write(0x00);
+            }
+            baos.write(rBytes);
+            for (int n = 0; n < 32 - sBytes.length; n++) {
+                baos.write(0x00);
+            }
+            baos.write(sBytes);
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+        return baos.toByteArray();
+    }
+
+    // Adds leading 0x00 if the first encoded byte MSB is set.
+    private static byte[] encodePositiveBigInteger(BigInteger i) {
+        byte[] bytes = i.toByteArray();
+        if ((bytes[0] & 0x80) != 0) {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            try {
+                baos.write(0x00);
+                baos.write(bytes);
+            } catch (IOException e) {
+                e.printStackTrace();
+                throw new RuntimeException("Failed writing data", e);
+            }
+            bytes = baos.toByteArray();
+        }
+        return bytes;
+    }
+
+    private static byte[] signatureCoseToDer(byte[] signature) {
+        if (signature.length != 64) {
+            throw new RuntimeException("signature.length is " + signature.length + ", expected 64");
+        }
+        BigInteger r = new BigInteger(Arrays.copyOfRange(signature, 0, 32));
+        BigInteger s = new BigInteger(Arrays.copyOfRange(signature, 32, 64));
+        byte[] rBytes = encodePositiveBigInteger(r);
+        byte[] sBytes = encodePositiveBigInteger(s);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            baos.write(0x30);
+            baos.write(2 + rBytes.length + 2 + sBytes.length);
+            baos.write(0x02);
+            baos.write(rBytes.length);
+            baos.write(rBytes);
+            baos.write(0x02);
+            baos.write(sBytes.length);
+            baos.write(sBytes);
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+        return baos.toByteArray();
+    }
+
+    public static byte[] coseSign1Sign(PrivateKey key,
+            @Nullable byte[] data,
+            byte[] detachedContent,
+            @Nullable Collection<X509Certificate> certificateChain)
+            throws NoSuchAlgorithmException, InvalidKeyException, CertificateEncodingException {
+
+        int dataLen = (data != null ? data.length : 0);
+        int detachedContentLen = (detachedContent != null ? detachedContent.length : 0);
+        if (dataLen > 0 && detachedContentLen > 0) {
+            throw new RuntimeException("data and detachedContent cannot both be non-empty");
+        }
+
+        CborBuilder protectedHeaders = new CborBuilder();
+        MapBuilder<CborBuilder> protectedHeadersMap = protectedHeaders.addMap();
+        protectedHeadersMap.put(COSE_LABEL_ALG, COSE_ALG_ECDSA_256);
+        byte[] protectedHeadersBytes = encodeCbor(protectedHeaders.build());
+
+        byte[] toBeSigned = coseBuildToBeSigned(protectedHeadersBytes, data, detachedContent);
+
+        byte[] coseSignature = null;
+        try {
+            Signature s = Signature.getInstance("SHA256withECDSA");
+            s.initSign(key);
+            s.update(toBeSigned);
+            byte[] derSignature = s.sign();
+            coseSignature = signatureDerToCose(derSignature);
+        } catch (SignatureException e) {
+            throw new RuntimeException("Error signing data");
+        }
+
+        CborBuilder builder = new CborBuilder();
+        ArrayBuilder<CborBuilder> array = builder.addArray();
+        array.add(protectedHeadersBytes);
+        MapBuilder<ArrayBuilder<CborBuilder>> unprotectedHeaders = array.addMap();
+        if (certificateChain != null && certificateChain.size() > 0) {
+            if (certificateChain.size() == 1) {
+                X509Certificate cert = certificateChain.iterator().next();
+                unprotectedHeaders.put(COSE_LABEL_X5CHAIN, cert.getEncoded());
+            } else {
+                ArrayBuilder<MapBuilder<ArrayBuilder<CborBuilder>>> x5chainsArray =
+                        unprotectedHeaders.putArray(COSE_LABEL_X5CHAIN);
+                for (X509Certificate cert : certificateChain) {
+                    x5chainsArray.add(cert.getEncoded());
+                }
+            }
+        }
+        if (data == null || data.length == 0) {
+            array.add(new SimpleValue(SimpleValueType.NULL));
+        } else {
+            array.add(data);
+        }
+        array.add(coseSignature);
+
+        return encodeCbor(builder.build());
+    }
+
+    public static boolean coseSign1CheckSignature(byte[] signatureCose1,
+            byte[] detachedContent,
+            PublicKey publicKey) throws NoSuchAlgorithmException, InvalidKeyException {
+        ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1);
+        List<DataItem> dataItems = null;
+        try {
+            dataItems = new CborDecoder(bais).decode();
+        } catch (CborException e) {
+            throw new RuntimeException("Given signature is not valid CBOR", e);
+        }
+        if (dataItems.size() != 1) {
+            throw new RuntimeException("Expected just one data item");
+        }
+        DataItem dataItem = dataItems.get(0);
+        if (dataItem.getMajorType() != MajorType.ARRAY) {
+            throw new RuntimeException("Data item is not an array");
+        }
+        List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems();
+        if (items.size() < 4) {
+            throw new RuntimeException("Expected at least four items in COSE_Sign1 array");
+        }
+        if (items.get(0).getMajorType() != MajorType.BYTE_STRING) {
+            throw new RuntimeException("Item 0 (protected headers) is not a byte-string");
+        }
+        byte[] encodedProtectedHeaders =
+                ((co.nstant.in.cbor.model.ByteString) items.get(0)).getBytes();
+        byte[] payload = new byte[0];
+        if (items.get(2).getMajorType() == MajorType.SPECIAL) {
+            if (((co.nstant.in.cbor.model.Special) items.get(2)).getSpecialType()
+                != SpecialType.SIMPLE_VALUE) {
+                throw new RuntimeException("Item 2 (payload) is a special but not a simple value");
+            }
+            SimpleValue simple = (co.nstant.in.cbor.model.SimpleValue) items.get(2);
+            if (simple.getSimpleValueType() != SimpleValueType.NULL) {
+                throw new RuntimeException("Item 2 (payload) is a simple but not the value null");
+            }
+        } else if (items.get(2).getMajorType() == MajorType.BYTE_STRING) {
+            payload = ((co.nstant.in.cbor.model.ByteString) items.get(2)).getBytes();
+        } else {
+            throw new RuntimeException("Item 2 (payload) is not nil or byte-string");
+        }
+        if (items.get(3).getMajorType() != MajorType.BYTE_STRING) {
+            throw new RuntimeException("Item 3 (signature) is not a byte-string");
+        }
+        byte[] coseSignature = ((co.nstant.in.cbor.model.ByteString) items.get(3)).getBytes();
+
+        byte[] derSignature = signatureCoseToDer(coseSignature);
+
+        int dataLen = payload.length;
+        int detachedContentLen = (detachedContent != null ? detachedContent.length : 0);
+        if (dataLen > 0 && detachedContentLen > 0) {
+            throw new RuntimeException("data and detachedContent cannot both be non-empty");
+        }
+
+        byte[] toBeSigned = Util.coseBuildToBeSigned(encodedProtectedHeaders,
+                payload, detachedContent);
+
+        try {
+            Signature verifier = Signature.getInstance("SHA256withECDSA");
+            verifier.initVerify(publicKey);
+            verifier.update(toBeSigned);
+            return verifier.verify(derSignature);
+        } catch (SignatureException e) {
+            throw new RuntimeException("Error verifying signature");
+        }
+    }
+
+    // Returns the empty byte-array if no data is included in the structure.
+    //
+    // Throws RuntimeException if the given bytes aren't valid COSE_Sign1.
+    //
+    public static byte[] coseSign1GetData(byte[] signatureCose1) {
+        ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1);
+        List<DataItem> dataItems = null;
+        try {
+            dataItems = new CborDecoder(bais).decode();
+        } catch (CborException e) {
+            throw new RuntimeException("Given signature is not valid CBOR", e);
+        }
+        if (dataItems.size() != 1) {
+            throw new RuntimeException("Expected just one data item");
+        }
+        DataItem dataItem = dataItems.get(0);
+        if (dataItem.getMajorType() != MajorType.ARRAY) {
+            throw new RuntimeException("Data item is not an array");
+        }
+        List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems();
+        if (items.size() < 4) {
+            throw new RuntimeException("Expected at least four items in COSE_Sign1 array");
+        }
+        byte[] payload = new byte[0];
+        if (items.get(2).getMajorType() == MajorType.SPECIAL) {
+            if (((co.nstant.in.cbor.model.Special) items.get(2)).getSpecialType()
+                != SpecialType.SIMPLE_VALUE) {
+                throw new RuntimeException("Item 2 (payload) is a special but not a simple value");
+            }
+            SimpleValue simple = (co.nstant.in.cbor.model.SimpleValue) items.get(2);
+            if (simple.getSimpleValueType() != SimpleValueType.NULL) {
+                throw new RuntimeException("Item 2 (payload) is a simple but not the value null");
+            }
+        } else if (items.get(2).getMajorType() == MajorType.BYTE_STRING) {
+            payload = ((co.nstant.in.cbor.model.ByteString) items.get(2)).getBytes();
+        } else {
+            throw new RuntimeException("Item 2 (payload) is not nil or byte-string");
+        }
+        return payload;
+    }
+
+    // Returns the empty collection if no x5chain is included in the structure.
+    //
+    // Throws RuntimeException if the given bytes aren't valid COSE_Sign1.
+    //
+    public static Collection<X509Certificate> coseSign1GetX5Chain(byte[] signatureCose1)
+            throws CertificateException {
+        ArrayList<X509Certificate> ret = new ArrayList<>();
+        ByteArrayInputStream bais = new ByteArrayInputStream(signatureCose1);
+        List<DataItem> dataItems = null;
+        try {
+            dataItems = new CborDecoder(bais).decode();
+        } catch (CborException e) {
+            throw new RuntimeException("Given signature is not valid CBOR", e);
+        }
+        if (dataItems.size() != 1) {
+            throw new RuntimeException("Expected just one data item");
+        }
+        DataItem dataItem = dataItems.get(0);
+        if (dataItem.getMajorType() != MajorType.ARRAY) {
+            throw new RuntimeException("Data item is not an array");
+        }
+        List<DataItem> items = ((co.nstant.in.cbor.model.Array) dataItem).getDataItems();
+        if (items.size() < 4) {
+            throw new RuntimeException("Expected at least four items in COSE_Sign1 array");
+        }
+        if (items.get(1).getMajorType() != MajorType.MAP) {
+            throw new RuntimeException("Item 1 (unprocted headers) is not a map");
+        }
+        co.nstant.in.cbor.model.Map map = (co.nstant.in.cbor.model.Map) items.get(1);
+        DataItem x5chainItem = map.get(new UnsignedInteger(COSE_LABEL_X5CHAIN));
+        if (x5chainItem != null) {
+            CertificateFactory factory = CertificateFactory.getInstance("X.509");
+            if (x5chainItem instanceof ByteString) {
+                ByteArrayInputStream certBais =
+                        new ByteArrayInputStream(((ByteString) x5chainItem).getBytes());
+                ret.add((X509Certificate) factory.generateCertificate(certBais));
+            } else if (x5chainItem instanceof Array) {
+                for (DataItem certItem : ((Array) x5chainItem).getDataItems()) {
+                    if (!(certItem instanceof ByteString)) {
+                        throw new RuntimeException(
+                            "Unexpected type for array item in x5chain value");
+                    }
+                    ByteArrayInputStream certBais =
+                            new ByteArrayInputStream(((ByteString) certItem).getBytes());
+                    ret.add((X509Certificate) factory.generateCertificate(certBais));
+                }
+            } else {
+                throw new RuntimeException("Unexpected type for x5chain value");
+            }
+        }
+        return ret;
+    }
+
+    public static byte[] coseBuildToBeMACed(byte[] encodedProtectedHeaders,
+            byte[] payload,
+            byte[] detachedContent) {
+        CborBuilder macStructure = new CborBuilder();
+        ArrayBuilder<CborBuilder> array = macStructure.addArray();
+
+        array.add("MAC0");
+        array.add(encodedProtectedHeaders);
+
+        // We currently don't support Externally Supplied Data (RFC 8152 section 4.3)
+        // so external_aad is the empty bstr
+        byte emptyExternalAad[] = new byte[0];
+        array.add(emptyExternalAad);
+
+        // Next field is the payload, independently of how it's transported (RFC
+        // 8152 section 4.4). Since our API specifies only one of |data| and
+        // |detachedContent| can be non-empty, it's simply just the non-empty one.
+        if (payload != null && payload.length > 0) {
+            array.add(payload);
+        } else {
+            array.add(detachedContent);
+        }
+
+        return encodeCbor(macStructure.build());
+    }
+
+    public static byte[] coseMac0(SecretKey key,
+            @Nullable byte[] data,
+            byte[] detachedContent)
+            throws NoSuchAlgorithmException, InvalidKeyException, CertificateEncodingException {
+
+        int dataLen = (data != null ? data.length : 0);
+        int detachedContentLen = (detachedContent != null ? detachedContent.length : 0);
+        if (dataLen > 0 && detachedContentLen > 0) {
+            throw new RuntimeException("data and detachedContent cannot both be non-empty");
+        }
+
+        CborBuilder protectedHeaders = new CborBuilder();
+        MapBuilder<CborBuilder> protectedHeadersMap = protectedHeaders.addMap();
+        protectedHeadersMap.put(COSE_LABEL_ALG, COSE_ALG_HMAC_256_256);
+        byte[] protectedHeadersBytes = encodeCbor(protectedHeaders.build());
+
+        byte[] toBeMACed = coseBuildToBeMACed(protectedHeadersBytes, data, detachedContent);
+
+        byte[] mac = null;
+        Mac m = Mac.getInstance("HmacSHA256");
+        m.init(key);
+        m.update(toBeMACed);
+        mac = m.doFinal();
+
+        CborBuilder builder = new CborBuilder();
+        ArrayBuilder<CborBuilder> array = builder.addArray();
+        array.add(protectedHeadersBytes);
+        MapBuilder<ArrayBuilder<CborBuilder>> unprotectedHeaders = array.addMap();
+        if (data == null || data.length == 0) {
+            array.add(new SimpleValue(SimpleValueType.NULL));
+        } else {
+            array.add(data);
+        }
+        array.add(mac);
+
+        return encodeCbor(builder.build());
+    }
+
+    public static String replaceLine(String text, int lineNumber, String replacementLine) {
+        String[] lines = text.split("\n");
+        int numLines = lines.length;
+        if (lineNumber < 0) {
+            lineNumber = numLines - (-lineNumber);
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int n = 0; n < numLines; n++) {
+            if (n == lineNumber) {
+                sb.append(replacementLine);
+            } else {
+                sb.append(lines[n]);
+            }
+            // Only add terminating newline if passed-in string ends in a newline.
+            if (n == numLines - 1) {
+                if (text.endsWith(("\n"))) {
+                    sb.append('\n');
+                }
+            } else {
+                sb.append('\n');
+            }
+        }
+        return sb.toString();
+    }
+
+    public static byte[] cborEncode(DataItem dataItem) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            new CborEncoder(baos).encode(dataItem);
+        } catch (CborException e) {
+            // This should never happen and we don't want cborEncode() to throw since that
+            // would complicate all callers. Log it instead.
+            e.printStackTrace();
+            Log.e(TAG, "Error encoding DataItem");
+        }
+        return baos.toByteArray();
+    }
+
+    public static byte[] cborEncodeBoolean(boolean value) {
+        return cborEncode(new CborBuilder().add(value).build().get(0));
+    }
+
+    public static byte[] cborEncodeString(@NonNull String value) {
+        return cborEncode(new CborBuilder().add(value).build().get(0));
+    }
+
+    public static byte[] cborEncodeBytestring(@NonNull byte[] value) {
+        return cborEncode(new CborBuilder().add(value).build().get(0));
+    }
+
+    public static byte[] cborEncodeInt(long value) {
+        return cborEncode(new CborBuilder().add(value).build().get(0));
+    }
+
+    static final int CBOR_SEMANTIC_TAG_ENCODED_CBOR = 24;
+
+    public static DataItem cborToDataItem(byte[] data) {
+        ByteArrayInputStream bais = new ByteArrayInputStream(data);
+        try {
+            List<DataItem> dataItems = new CborDecoder(bais).decode();
+            if (dataItems.size() != 1) {
+                throw new RuntimeException("Expected 1 item, found " + dataItems.size());
+            }
+            return dataItems.get(0);
+        } catch (CborException e) {
+            throw new RuntimeException("Error decoding data", e);
+        }
+    }
+
+    public static boolean cborDecodeBoolean(@NonNull byte[] data) {
+        return cborToDataItem(data) == SimpleValue.TRUE;
+    }
+
+    public static String cborDecodeString(@NonNull byte[] data) {
+        return ((co.nstant.in.cbor.model.UnicodeString) cborToDataItem(data)).getString();
+    }
+
+    public static long cborDecodeInt(@NonNull byte[] data) {
+        return ((co.nstant.in.cbor.model.Number) cborToDataItem(data)).getValue().longValue();
+    }
+
+    public static byte[] cborDecodeBytestring(@NonNull byte[] data) {
+        return ((co.nstant.in.cbor.model.ByteString) cborToDataItem(data)).getBytes();
+    }
+
+    public static String getStringEntry(ResultData data, String namespaceName, String name) {
+        return Util.cborDecodeString(data.getEntry(namespaceName, name));
+    }
+
+    public static boolean getBooleanEntry(ResultData data, String namespaceName, String name) {
+        return Util.cborDecodeBoolean(data.getEntry(namespaceName, name));
+    }
+
+    public static long getIntegerEntry(ResultData data, String namespaceName, String name) {
+        return Util.cborDecodeInt(data.getEntry(namespaceName, name));
+    }
+
+    public static byte[] getBytestringEntry(ResultData data, String namespaceName, String name) {
+        return Util.cborDecodeBytestring(data.getEntry(namespaceName, name));
+    }
+
+    /*
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 1 (0x1)
+    Signature Algorithm: ecdsa-with-SHA256
+        Issuer: CN=fake
+        Validity
+            Not Before: Jan  1 00:00:00 1970 GMT
+            Not After : Jan  1 00:00:00 2048 GMT
+        Subject: CN=fake
+        Subject Public Key Info:
+            Public Key Algorithm: id-ecPublicKey
+                Public-Key: (256 bit)
+                00000000  04 9b 60 70 8a 99 b6 bf  e3 b8 17 02 9e 93 eb 48  |..`p...........H|
+                00000010  23 b9 39 89 d1 00 bf a0  0f d0 2f bd 6b 11 bc d1  |#.9......./.k...|
+                00000020  19 53 54 28 31 00 f5 49  db 31 fb 9f 7d 99 bf 23  |.ST(1..I.1..}..#|
+                00000030  fb 92 04 6b 23 63 55 98  ad 24 d2 68 c4 83 bf 99  |...k#cU..$.h....|
+                00000040  62                                                |b|
+    Signature Algorithm: ecdsa-with-SHA256
+         30:45:02:20:67:ad:d1:34:ed:a5:68:3f:5b:33:ee:b3:18:a2:
+         eb:03:61:74:0f:21:64:4a:a3:2e:82:b3:92:5c:21:0f:88:3f:
+         02:21:00:b7:38:5c:9b:f2:9c:b1:27:86:37:44:df:eb:4a:b2:
+         6c:11:9a:c1:ff:b2:80:95:ce:fc:5f:26:b4:20:6e:9b:0d
+     */
+
+
+    public static @NonNull X509Certificate signPublicKeyWithPrivateKey(String keyToSignAlias,
+            String keyToSignWithAlias) {
+
+        KeyStore ks = null;
+        try {
+            ks = KeyStore.getInstance("AndroidKeyStore");
+            ks.load(null);
+
+            /* First note that KeyStore.getCertificate() returns a self-signed X.509 certificate
+             * for the key in question. As per RFC 5280, section 4.1 an X.509 certificate has the
+             * following structure:
+             *
+             *   Certificate  ::=  SEQUENCE  {
+             *        tbsCertificate       TBSCertificate,
+             *        signatureAlgorithm   AlgorithmIdentifier,
+             *        signatureValue       BIT STRING  }
+             *
+             * Conveniently, the X509Certificate class has a getTBSCertificate() method which
+             * returns the tbsCertificate blob. So all we need to do is just sign that and build
+             * signatureAlgorithm and signatureValue and combine it with tbsCertificate. We don't
+             * need a full-blown ASN.1/DER encoder to do this.
+             */
+            X509Certificate selfSignedCert = (X509Certificate) ks.getCertificate(keyToSignAlias);
+            byte[] tbsCertificate = selfSignedCert.getTBSCertificate();
+
+            KeyStore.Entry keyToSignWithEntry = ks.getEntry(keyToSignWithAlias, null);
+            Signature s = Signature.getInstance("SHA256withECDSA");
+            s.initSign(((KeyStore.PrivateKeyEntry) keyToSignWithEntry).getPrivateKey());
+            s.update(tbsCertificate);
+            byte[] signatureValue = s.sign();
+
+            /* The DER encoding for a SEQUENCE of length 128-65536 - the length is updated below.
+             *
+             * We assume - and test for below - that the final length is always going to be in
+             * this range. This is a sound assumption given we're using 256-bit EC keys.
+             */
+            byte[] sequence = new byte[]{
+                    0x30, (byte) 0x82, 0x00, 0x00
+            };
+
+            /* The DER encoding for the ECDSA with SHA-256 signature algorithm:
+             *
+             *   SEQUENCE (1 elem)
+             *      OBJECT IDENTIFIER 1.2.840.10045.4.3.2 ecdsaWithSHA256 (ANSI X9.62 ECDSA
+             *      algorithm with SHA256)
+             */
+            byte[] signatureAlgorithm = new byte[]{
+                    0x30, 0x0a, 0x06, 0x08, 0x2a, (byte) 0x86, 0x48, (byte) 0xce, 0x3d, 0x04, 0x03,
+                    0x02
+            };
+
+            /* The DER encoding for a BIT STRING with one element - the length is updated below.
+             *
+             * We assume the length of signatureValue is always going to be less than 128. This
+             * assumption works since we know ecdsaWithSHA256 signatures are always 69, 70, or
+             * 71 bytes long when DER encoded.
+             */
+            byte[] bitStringForSignature = new byte[]{0x03, 0x00, 0x00};
+
+            // Calculate sequence length and set it in |sequence|.
+            int sequenceLength = tbsCertificate.length
+                    + signatureAlgorithm.length
+                    + bitStringForSignature.length
+                    + signatureValue.length;
+            if (sequenceLength < 128 || sequenceLength > 65535) {
+                throw new Exception("Unexpected sequenceLength " + sequenceLength);
+            }
+            sequence[2] = (byte) (sequenceLength >> 8);
+            sequence[3] = (byte) (sequenceLength & 0xff);
+
+            // Calculate signatureValue length and set it in |bitStringForSignature|.
+            int signatureValueLength = signatureValue.length + 1;
+            if (signatureValueLength >= 128) {
+                throw new Exception("Unexpected signatureValueLength " + signatureValueLength);
+            }
+            bitStringForSignature[1] = (byte) signatureValueLength;
+
+            // Finally concatenate everything together.
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            baos.write(sequence);
+            baos.write(tbsCertificate);
+            baos.write(signatureAlgorithm);
+            baos.write(bitStringForSignature);
+            baos.write(signatureValue);
+            byte[] resultingCertBytes = baos.toByteArray();
+
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            ByteArrayInputStream bais = new ByteArrayInputStream(resultingCertBytes);
+            X509Certificate result = (X509Certificate) cf.generateCertificate(bais);
+            return result;
+        } catch (Exception e) {
+            throw new RuntimeException("Error signing public key with private key", e);
+        }
+    }
+
+    public static byte[] buildDeviceAuthenticationCbor(String docType,
+            byte[] encodedSessionTranscript,
+            byte[] deviceNameSpacesBytes) {
+        ByteArrayOutputStream daBaos = new ByteArrayOutputStream();
+        try {
+            ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript);
+            List<DataItem> dataItems = null;
+            dataItems = new CborDecoder(bais).decode();
+            DataItem sessionTranscript = dataItems.get(0);
+            ByteString deviceNameSpacesBytesItem = new ByteString(deviceNameSpacesBytes);
+            deviceNameSpacesBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+            new CborEncoder(daBaos).encode(new CborBuilder()
+                    .addArray()
+                    .add("DeviceAuthentication")
+                    .add(sessionTranscript)
+                    .add(docType)
+                    .add(deviceNameSpacesBytesItem)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding DeviceAuthentication", e);
+        }
+        return daBaos.toByteArray();
+    }
+
+    public static byte[] buildReaderAuthenticationBytesCbor(
+            byte[] encodedSessionTranscript,
+            byte[] requestMessageBytes) {
+
+        ByteArrayOutputStream daBaos = new ByteArrayOutputStream();
+        try {
+            ByteArrayInputStream bais = new ByteArrayInputStream(encodedSessionTranscript);
+            List<DataItem> dataItems = null;
+            dataItems = new CborDecoder(bais).decode();
+            DataItem sessionTranscript = dataItems.get(0);
+            ByteString requestMessageBytesItem = new ByteString(requestMessageBytes);
+            requestMessageBytesItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+            new CborEncoder(daBaos).encode(new CborBuilder()
+                    .addArray()
+                    .add("ReaderAuthentication")
+                    .add(sessionTranscript)
+                    .add(requestMessageBytesItem)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding ReaderAuthentication", e);
+        }
+        byte[] readerAuthentication = daBaos.toByteArray();
+        return Util.prependSemanticTagForEncodedCbor(readerAuthentication);
+    }
+
+    // Returns #6.24(bstr) of the given already encoded CBOR
+    //
+    public static @NonNull DataItem buildCborTaggedByteString(@NonNull byte[] encodedCbor) {
+        DataItem item = new ByteString(encodedCbor);
+        item.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+        return item;
+    }
+
+    public static byte[] prependSemanticTagForEncodedCbor(byte[] encodedCbor) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            new CborEncoder(baos).encode(buildCborTaggedByteString(encodedCbor));
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding with semantic tag for CBOR encoding", e);
+        }
+        return baos.toByteArray();
+    }
+
+    public static byte[] concatArrays(byte[] a, byte[] b) {
+        byte[] ret = new byte[a.length + b.length];
+        System.arraycopy(a, 0, ret, 0, a.length);
+        System.arraycopy(b, 0, ret, a.length, b.length);
+        return ret;
+    }
+
+    public static SecretKey calcEMacKeyForReader(PublicKey authenticationPublicKey,
+            PrivateKey ephemeralReaderPrivateKey,
+            byte[] encodedSessionTranscript) {
+        try {
+            KeyAgreement ka = KeyAgreement.getInstance("ECDH");
+            ka.init(ephemeralReaderPrivateKey);
+            ka.doPhase(authenticationPublicKey, true);
+            byte[] sharedSecret = ka.generateSecret();
+
+            byte[] sessionTranscriptBytes =
+                    Util.prependSemanticTagForEncodedCbor(encodedSessionTranscript);
+
+            byte[] salt = MessageDigest.getInstance("SHA-256").digest(sessionTranscriptBytes);
+            byte[] info = new byte[] {'E', 'M', 'a', 'c', 'K', 'e', 'y'};
+            byte[] derivedKey = Util.computeHkdf("HmacSha256", sharedSecret, salt, info, 32);
+            SecretKey secretKey = new SecretKeySpec(derivedKey, "");
+            return secretKey;
+        } catch (InvalidKeyException
+                | NoSuchAlgorithmException e) {
+            throw new RuntimeException("Error performing key agreement", e);
+        }
+    }
+
+    /**
+     * Computes an HKDF.
+     *
+     * This is based on https://github.com/google/tink/blob/master/java/src/main/java/com/google
+     * /crypto/tink/subtle/Hkdf.java
+     * which is also Copyright (c) Google and also licensed under the Apache 2 license.
+     *
+     * @param macAlgorithm the MAC algorithm used for computing the Hkdf. I.e., "HMACSHA1" or
+     *                     "HMACSHA256".
+     * @param ikm          the input keying material.
+     * @param salt         optional salt. A possibly non-secret random value. If no salt is
+     *                     provided (i.e. if
+     *                     salt has length 0) then an array of 0s of the same size as the hash
+     *                     digest is used as salt.
+     * @param info         optional context and application specific information.
+     * @param size         The length of the generated pseudorandom string in bytes. The maximal
+     *                     size is
+     *                     255.DigestSize, where DigestSize is the size of the underlying HMAC.
+     * @return size pseudorandom bytes.
+     */
+    public static byte[] computeHkdf(
+            String macAlgorithm, final byte[] ikm, final byte[] salt, final byte[] info, int size) {
+        Mac mac = null;
+        try {
+            mac = Mac.getInstance(macAlgorithm);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("No such algorithm: " + macAlgorithm, e);
+        }
+        if (size > 255 * mac.getMacLength()) {
+            throw new RuntimeException("size too large");
+        }
+        try {
+            if (salt == null || salt.length == 0) {
+                // According to RFC 5869, Section 2.2 the salt is optional. If no salt is provided
+                // then HKDF uses a salt that is an array of zeros of the same length as the hash
+                // digest.
+                mac.init(new SecretKeySpec(new byte[mac.getMacLength()], macAlgorithm));
+            } else {
+                mac.init(new SecretKeySpec(salt, macAlgorithm));
+            }
+            byte[] prk = mac.doFinal(ikm);
+            byte[] result = new byte[size];
+            int ctr = 1;
+            int pos = 0;
+            mac.init(new SecretKeySpec(prk, macAlgorithm));
+            byte[] digest = new byte[0];
+            while (true) {
+                mac.update(digest);
+                mac.update(info);
+                mac.update((byte) ctr);
+                digest = mac.doFinal();
+                if (pos + digest.length < size) {
+                    System.arraycopy(digest, 0, result, pos, digest.length);
+                    pos += digest.length;
+                    ctr++;
+                } else {
+                    System.arraycopy(digest, 0, result, pos, size - pos);
+                    break;
+                }
+            }
+            return result;
+        } catch (InvalidKeyException e) {
+            throw new RuntimeException("Error MACing", e);
+        }
+    }
+
+    static byte[] stripLeadingZeroes(byte[] value) {
+        int n = 0;
+        while (n < value.length && value[n] == 0) {
+            n++;
+        }
+        int newLen = value.length - n;
+        byte[] ret = new byte[newLen];
+        int m = 0;
+        while (n < value.length) {
+            ret[m++] = value[n++];
+        }
+        return ret;
+    }
+
+    public static void hexdump(String name, byte[] data) {
+        int n, m, o;
+        StringBuilder sb = new StringBuilder();
+        Formatter fmt = new Formatter(sb);
+        for (n = 0; n < data.length; n += 16) {
+            fmt.format("%04x  ", n);
+            for (m = 0; m < 16 && n + m < data.length; m++) {
+                fmt.format("%02x ", data[n + m]);
+            }
+            for (o = m; o < 16; o++) {
+                sb.append("   ");
+            }
+            sb.append(" ");
+            for (m = 0; m < 16 && n + m < data.length; m++) {
+                int c = data[n + m] & 0xff;
+                fmt.format("%c", Character.isISOControl(c) ? '.' : c);
+            }
+            sb.append("\n");
+        }
+        sb.append("\n");
+        Log.e(TAG, name + ": dumping " + data.length + " bytes\n" + fmt.toString());
+    }
+
+
+    // This returns a SessionTranscript which satisfy the requirement
+    // that the uncompressed X and Y coordinates of the public key for the
+    // mDL's ephemeral key-pair appear somewhere in the encoded
+    // DeviceEngagement.
+    public static byte[] buildSessionTranscript(KeyPair ephemeralKeyPair) {
+        // Make the coordinates appear in an already encoded bstr - this
+        // mimics how the mDL COSE_Key appear as encoded data inside the
+        // encoded DeviceEngagement
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            ECPoint w = ((ECPublicKey) ephemeralKeyPair.getPublic()).getW();
+            // X and Y are always positive so for interop we remove any leading zeroes
+            // inserted by the BigInteger encoder.
+            byte[] x = stripLeadingZeroes(w.getAffineX().toByteArray());
+            byte[] y = stripLeadingZeroes(w.getAffineY().toByteArray());
+            baos.write(new byte[]{42});
+            baos.write(x);
+            baos.write(y);
+            baos.write(new byte[]{43, 44});
+        } catch (IOException e) {
+            e.printStackTrace();
+            return null;
+        }
+        byte[] blobWithCoords = baos.toByteArray();
+
+        baos = new ByteArrayOutputStream();
+        try {
+            new CborEncoder(baos).encode(new CborBuilder()
+                    .addArray()
+                    .add(blobWithCoords)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            e.printStackTrace();
+            return null;
+        }
+        ByteString encodedDeviceEngagementItem = new ByteString(baos.toByteArray());
+        ByteString encodedEReaderKeyItem = new ByteString(cborEncodeString("doesn't matter"));
+        encodedDeviceEngagementItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+        encodedEReaderKeyItem.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+
+        baos = new ByteArrayOutputStream();
+        try {
+            new CborEncoder(baos).encode(new CborBuilder()
+                    .addArray()
+                    .add(encodedDeviceEngagementItem)
+                    .add(encodedEReaderKeyItem)
+                    .end()
+                    .build());
+        } catch (CborException e) {
+            e.printStackTrace();
+            return null;
+        }
+        return baos.toByteArray();
+    }
+
+    /*
+     * Helper function to create a CBOR data for requesting data items. The IntentToRetain
+     * value will be set to false for all elements.
+     *
+     * <p>The returned CBOR data conforms to the following CDDL schema:</p>
+     *
+     * <pre>
+     *   ItemsRequest = {
+     *     ? "docType" : DocType,
+     *     "nameSpaces" : NameSpaces,
+     *     ? "RequestInfo" : {* tstr => any} ; Additional info the reader wants to provide
+     *   }
+     *
+     *   NameSpaces = {
+     *     + NameSpace => DataElements     ; Requested data elements for each NameSpace
+     *   }
+     *
+     *   DataElements = {
+     *     + DataElement => IntentToRetain
+     *   }
+     *
+     *   DocType = tstr
+     *
+     *   DataElement = tstr
+     *   IntentToRetain = bool
+     *   NameSpace = tstr
+     * </pre>
+     *
+     * @param entriesToRequest       The entries to request, organized as a map of namespace
+     *                               names with each value being a collection of data elements
+     *                               in the given namespace.
+     * @param docType                  The document type or {@code null} if there is no document
+     *                                 type.
+     * @return CBOR data conforming to the CDDL mentioned above.
+     */
+    public static @NonNull byte[] createItemsRequest(
+            @NonNull Map<String, Collection<String>> entriesToRequest,
+            @Nullable String docType) {
+        CborBuilder builder = new CborBuilder();
+        MapBuilder<CborBuilder> mapBuilder = builder.addMap();
+        if (docType != null) {
+            mapBuilder.put("docType", docType);
+        }
+
+        MapBuilder<MapBuilder<CborBuilder>> nsMapBuilder = mapBuilder.putMap("nameSpaces");
+        for (String namespaceName : entriesToRequest.keySet()) {
+            Collection<String> entryNames = entriesToRequest.get(namespaceName);
+            MapBuilder<MapBuilder<MapBuilder<CborBuilder>>> entryNameMapBuilder =
+                    nsMapBuilder.putMap(namespaceName);
+            for (String entryName : entryNames) {
+                entryNameMapBuilder.put(entryName, false);
+            }
+        }
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        CborEncoder encoder = new CborEncoder(baos);
+        try {
+            encoder.encode(builder.build());
+        } catch (CborException e) {
+            throw new RuntimeException("Error encoding CBOR", e);
+        }
+        return baos.toByteArray();
+    }
+
+    public static KeyPair createEphemeralKeyPair() {
+        try {
+            KeyPairGenerator kpg = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC);
+            ECGenParameterSpec ecSpec = new ECGenParameterSpec("prime256v1");
+            kpg.initialize(ecSpec);
+            KeyPair keyPair = kpg.generateKeyPair();
+            return keyPair;
+        } catch (NoSuchAlgorithmException
+                | InvalidAlgorithmParameterException e) {
+            throw new RuntimeException("Error generating ephemeral key-pair", e);
+        }
+    }
+
+    public static byte[] getPopSha256FromAuthKeyCert(X509Certificate cert) {
+        byte[] octetString = cert.getExtensionValue("1.3.6.1.4.1.11129.2.1.26");
+        if (octetString == null) {
+            return null;
+        }
+        Util.hexdump("octetString", octetString);
+
+        try {
+            ASN1InputStream asn1InputStream = new ASN1InputStream(octetString);
+            byte[] cborBytes = ((ASN1OctetString) asn1InputStream.readObject()).getOctets();
+            Util.hexdump("cborBytes", cborBytes);
+
+            ByteArrayInputStream bais = new ByteArrayInputStream(cborBytes);
+            List<DataItem> dataItems = new CborDecoder(bais).decode();
+            if (dataItems.size() != 1) {
+                throw new RuntimeException("Expected 1 item, found " + dataItems.size());
+            }
+            if (!(dataItems.get(0) instanceof co.nstant.in.cbor.model.Array)) {
+                throw new RuntimeException("Item is not a map");
+            }
+            co.nstant.in.cbor.model.Array array = (co.nstant.in.cbor.model.Array) dataItems.get(0);
+            List<DataItem> items = array.getDataItems();
+            if (items.size() < 2) {
+                throw new RuntimeException(
+                        "Expected at least 2 array items, found " + items.size());
+            }
+            if (!(items.get(0) instanceof UnicodeString)) {
+                throw new RuntimeException("First array item is not a string");
+            }
+            String id = ((UnicodeString) items.get(0)).getString();
+            if (!id.equals("ProofOfBinding")) {
+                throw new RuntimeException("Expected ProofOfBinding, got " + id);
+            }
+            if (!(items.get(1) instanceof ByteString)) {
+                throw new RuntimeException("Second array item is not a bytestring");
+            }
+            byte[] popSha256 = ((ByteString) items.get(1)).getBytes();
+            if (popSha256.length != 32) {
+                throw new RuntimeException(
+                        "Expected bstr to be 32 bytes, it is " + popSha256.length);
+            }
+            return popSha256;
+        } catch (IOException e) {
+            throw new RuntimeException("Error decoding extension data", e);
+        } catch (CborException e) {
+            throw new RuntimeException("Error decoding data", e);
+        }
+    }
+
+}
diff --git a/identity/util/test/java/com/android/security/identity/internal/HkdfTest.java b/identity/util/test/java/com/android/security/identity/internal/HkdfTest.java
new file mode 100644
index 0000000..6a75090
--- /dev/null
+++ b/identity/util/test/java/com/android/security/identity/internal/HkdfTest.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2019 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.security.identity.internal;
+
+import androidx.test.runner.AndroidJUnit4;
+import com.android.security.identity.internal.Util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.GeneralSecurityException;
+import java.util.Random;
+
+/*
+ * This is based on https://github.com/google/tink/blob/master/java/src/test/java/com/google
+ * /crypto/tink/subtle/HkdfTest.java
+ * which is also Copyright (c) Google and licensed under the Apache 2 license.
+ */
+@RunWith(AndroidJUnit4.class)
+public class HkdfTest {
+
+    static Random sRandom = new Random();
+
+    /** Encodes a byte array to hex. */
+    static String hexEncode(final byte[] bytes) {
+        String chars = "0123456789abcdef";
+        StringBuilder result = new StringBuilder(2 * bytes.length);
+        for (byte b : bytes) {
+            // convert to unsigned
+            int val = b & 0xff;
+            result.append(chars.charAt(val / 16));
+            result.append(chars.charAt(val % 16));
+        }
+        return result.toString();
+    }
+
+    /** Decodes a hex string to a byte array. */
+    static byte[] hexDecode(String hex) {
+        if (hex.length() % 2 != 0) {
+            throw new IllegalArgumentException("Expected a string of even length");
+        }
+        int size = hex.length() / 2;
+        byte[] result = new byte[size];
+        for (int i = 0; i < size; i++) {
+            int hi = Character.digit(hex.charAt(2 * i), 16);
+            int lo = Character.digit(hex.charAt(2 * i + 1), 16);
+            if ((hi == -1) || (lo == -1)) {
+                throw new IllegalArgumentException("input is not hexadecimal");
+            }
+            result[i] = (byte) (16 * hi + lo);
+        }
+        return result;
+    }
+
+    static byte[] randBytes(int numBytes) {
+        byte[] bytes = new byte[numBytes];
+        sRandom.nextBytes(bytes);
+        return bytes;
+    }
+
+    @Test
+    public void testNullSaltOrInfo() throws Exception {
+        byte[] ikm = randBytes(20);
+        byte[] info = randBytes(20);
+        int size = 40;
+
+        byte[] hkdfWithNullSalt = Util.computeHkdf("HmacSha256", ikm, null, info, size);
+        byte[] hkdfWithEmptySalt = Util.computeHkdf("HmacSha256", ikm, new byte[0], info, size);
+        assertArrayEquals(hkdfWithNullSalt, hkdfWithEmptySalt);
+
+        byte[] salt = randBytes(20);
+        byte[] hkdfWithNullInfo = Util.computeHkdf("HmacSha256", ikm, salt, null, size);
+        byte[] hkdfWithEmptyInfo = Util.computeHkdf("HmacSha256", ikm, salt, new byte[0], size);
+        assertArrayEquals(hkdfWithNullInfo, hkdfWithEmptyInfo);
+    }
+
+    @Test
+    public void testInvalidCodeSize() throws Exception {
+        try {
+            Util.computeHkdf("HmacSha256", new byte[0], new byte[0], new byte[0], 32 * 256);
+            fail("Invalid size, should have thrown exception");
+        } catch (RuntimeException expected) {
+
+            // Expected
+        }
+    }
+
+    /**
+     * Tests the implementation against the test vectors from RFC 5869.
+     */
+    @Test
+    public void testVectors() throws Exception {
+        // Test case 1
+        assertEquals(
+                "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf"
+                + "1a5a4c5db02d56ecc4c5bf34007208d5b887185865",
+                computeHkdfHex("HmacSha256",
+                        "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+                        "000102030405060708090a0b0c",
+                        "f0f1f2f3f4f5f6f7f8f9",
+                        42));
+
+        // Test case 2
+        assertEquals(
+                "b11e398dc80327a1c8e7f78c596a49344f012eda2d4efad8a050cc4c19afa97c"
+                        + "59045a99cac7827271cb41c65e590e09da3275600c2f09b8367793a9aca3db71"
+                        + "cc30c58179ec3e87c14c01d5c1f3434f1d87",
+                computeHkdfHex("HmacSha256",
+                        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
+                                + "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"
+                                + "404142434445464748494a4b4c4d4e4f",
+                        "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f"
+                                + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"
+                                + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+                        "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
+                                + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef"
+                                + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+                        82));
+
+        // Test case 3: salt is empty
+        assertEquals(
+                "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d"
+                        + "9d201395faa4b61a96c8",
+                computeHkdfHex("HmacSha256",
+                        "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", "", "",
+                        42));
+
+        // Test Case 4
+        assertEquals(
+                "085a01ea1b10f36933068b56efa5ad81a4f14b822f"
+                + "5b091568a9cdd4f155fda2c22e422478d305f3f896",
+                computeHkdfHex(
+                        "HmacSha1",
+                        "0b0b0b0b0b0b0b0b0b0b0b",
+                        "000102030405060708090a0b0c",
+                        "f0f1f2f3f4f5f6f7f8f9",
+                        42));
+
+        // Test Case 5
+        assertEquals(
+                "0bd770a74d1160f7c9f12cd5912a06ebff6adcae899d92191fe4305673ba2ffe"
+                        + "8fa3f1a4e5ad79f3f334b3b202b2173c486ea37ce3d397ed034c7f9dfeb15c5e"
+                        + "927336d0441f4c4300e2cff0d0900b52d3b4",
+                computeHkdfHex(
+                        "HmacSha1",
+                        "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
+                                + "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"
+                                + "404142434445464748494a4b4c4d4e4f",
+                        "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f"
+                                + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"
+                                + "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf",
+                        "b0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf"
+                                + "d0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeef"
+                                + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff",
+                        82));
+
+        // Test Case 6: salt is empty
+        assertEquals(
+                "0ac1af7002b3d761d1e55298da9d0506b9ae52057220a306e07b6b87e8df21d0"
+                        + "ea00033de03984d34918",
+                computeHkdfHex("HmacSha1", "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", "", "",
+                        42));
+
+        // Test Case 7
+        assertEquals(
+                "2c91117204d745f3500d636a62f64f0ab3bae548aa53d423b0d1f27ebba6f5e5"
+                        + "673a081d70cce7acfc48",
+                computeHkdfHex("HmacSha1", "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c", "", "",
+                        42));
+    }
+
+    /**
+     * Test version of Hkdf where all inputs and outputs are hexadecimal.
+     */
+    private String computeHkdfHex(String macAlgorithm, String ikmHex, String saltHex,
+            String infoHex,
+            int size) throws GeneralSecurityException {
+        return hexEncode(
+                Util.computeHkdf(macAlgorithm, hexDecode(ikmHex), hexDecode(saltHex),
+                        hexDecode(infoHex), size));
+    }
+
+}
diff --git a/identity/util/test/java/com/android/security/identity/internal/UtilUnitTests.java b/identity/util/test/java/com/android/security/identity/internal/UtilUnitTests.java
new file mode 100644
index 0000000..9c27c14
--- /dev/null
+++ b/identity/util/test/java/com/android/security/identity/internal/UtilUnitTests.java
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2019 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.security.identity.internal;
+
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import android.security.keystore.KeyGenParameterSpec;
+import android.security.keystore.KeyProperties;
+import com.android.security.identity.internal.Util;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import java.security.cert.X509Certificate;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import co.nstant.in.cbor.CborBuilder;
+import co.nstant.in.cbor.CborDecoder;
+import co.nstant.in.cbor.CborEncoder;
+import co.nstant.in.cbor.CborException;
+import co.nstant.in.cbor.builder.ArrayBuilder;
+import co.nstant.in.cbor.model.ByteString;
+import co.nstant.in.cbor.model.DataItem;
+import co.nstant.in.cbor.model.DoublePrecisionFloat;
+import co.nstant.in.cbor.model.HalfPrecisionFloat;
+import co.nstant.in.cbor.model.NegativeInteger;
+import co.nstant.in.cbor.model.SimpleValue;
+import co.nstant.in.cbor.model.SimpleValueType;
+import co.nstant.in.cbor.model.SinglePrecisionFloat;
+import co.nstant.in.cbor.model.UnicodeString;
+import co.nstant.in.cbor.model.UnsignedInteger;
+
+@RunWith(AndroidJUnit4.class)
+public class UtilUnitTests {
+    @Test
+    public void prettyPrintMultipleCompleteTypes() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new CborBuilder()
+                .add("text")                // add string
+                .add(1234)                  // add integer
+                .add(new byte[]{0x10})   // add byte array
+                .addArray()                 // add array
+                .add(1)
+                .add("text")
+                .end()
+                .build());
+        assertEquals("'text',\n"
+                + "1234,\n"
+                + "[0x10],\n"
+                + "[1, 'text']", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintString() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new UnicodeString("foobar"));
+        assertEquals("'foobar'", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintBytestring() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new ByteString(new byte[]{1, 2, 33, (byte) 254}));
+        assertEquals("[0x01, 0x02, 0x21, 0xfe]", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintUnsignedInteger() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new UnsignedInteger(42));
+        assertEquals("42", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintNegativeInteger() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new NegativeInteger(-42));
+        assertEquals("-42", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintDouble() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new DoublePrecisionFloat(1.1));
+        assertEquals("1.1", Util.cborPrettyPrint(baos.toByteArray()));
+
+        baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new DoublePrecisionFloat(-42.0000000001));
+        assertEquals("-42.0000000001", Util.cborPrettyPrint(baos.toByteArray()));
+
+        baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new DoublePrecisionFloat(-5));
+        assertEquals("-5", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintFloat() throws CborException {
+        ByteArrayOutputStream baos;
+
+        // TODO: These two tests yield different results on different devices, disable for now
+        /*
+        baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new SinglePrecisionFloat(1.1f));
+        assertEquals("1.100000023841858", Util.cborPrettyPrint(baos.toByteArray()));
+
+        baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new SinglePrecisionFloat(-42.0001f));
+        assertEquals("-42.000099182128906", Util.cborPrettyPrint(baos.toByteArray()));
+        */
+
+        baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new SinglePrecisionFloat(-5f));
+        assertEquals("-5", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintHalfFloat() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new HalfPrecisionFloat(1.1f));
+        assertEquals("1.099609375", Util.cborPrettyPrint(baos.toByteArray()));
+
+        baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new HalfPrecisionFloat(-42.0001f));
+        assertEquals("-42", Util.cborPrettyPrint(baos.toByteArray()));
+
+        baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new HalfPrecisionFloat(-5f));
+        assertEquals("-5", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintFalse() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new SimpleValue(SimpleValueType.FALSE));
+        assertEquals("false", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintTrue() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new SimpleValue(SimpleValueType.TRUE));
+        assertEquals("true", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintNull() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new SimpleValue(SimpleValueType.NULL));
+        assertEquals("null", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintUndefined() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new SimpleValue(SimpleValueType.UNDEFINED));
+        assertEquals("undefined", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintTag() throws CborException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new CborBuilder()
+                        .addTag(0)
+                        .add("ABC")
+                        .build());
+        byte[] data = baos.toByteArray();
+        assertEquals("tag 0 'ABC'", Util.cborPrettyPrint(data));
+    }
+
+    @Test
+    public void prettyPrintArrayNoCompounds() throws CborException {
+        // If an array has no compound elements, no newlines are used.
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new CborBuilder()
+                .addArray()                 // add array
+                .add(1)
+                .add("text")
+                .add(new ByteString(new byte[]{1, 2, 3}))
+                .end()
+                .build());
+        assertEquals("[1, 'text', [0x01, 0x02, 0x03]]", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintArray() throws CborException {
+        // This array contains a compound value so will use newlines
+        CborBuilder array = new CborBuilder();
+        ArrayBuilder<CborBuilder> arrayBuilder = array.addArray();
+        arrayBuilder.add(2);
+        arrayBuilder.add(3);
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new CborBuilder()
+                .addArray()                 // add array
+                .add(1)
+                .add("text")
+                .add(new ByteString(new byte[]{1, 2, 3}))
+                .add(array.build().get(0))
+                .end()
+                .build());
+        assertEquals("[\n"
+                + "  1,\n"
+                + "  'text',\n"
+                + "  [0x01, 0x02, 0x03],\n"
+                + "  [2, 3]\n"
+                + "]", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void prettyPrintMap() throws CborException {
+        // If an array has no compound elements, no newlines are used.
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        new CborEncoder(baos).encode(new CborBuilder()
+                .addMap()
+                .put("Foo", 42)
+                .put("Bar", "baz")
+                .put(43, 44)
+                .put(new UnicodeString("bstr"), new ByteString(new byte[]{1, 2, 3}))
+                .put(new ByteString(new byte[]{1, 2, 3}), new UnicodeString("other way"))
+                .end()
+                .build());
+        assertEquals("{\n"
+                + "  43 : 44,\n"
+                + "  [0x01, 0x02, 0x03] : 'other way',\n"
+                + "  'Bar' : 'baz',\n"
+                + "  'Foo' : 42,\n"
+                + "  'bstr' : [0x01, 0x02, 0x03]\n"
+                + "}", Util.cborPrettyPrint(baos.toByteArray()));
+    }
+
+    @Test
+    public void testCanonicalizeCbor() throws Exception {
+        // {"one":1, 2:"two"}
+        byte[] first =
+                new byte[]{(byte) 0xA2, 0x63, 0x6F, 0x6E, 0x65, 0x01, 0x02, 0x63, 0x74, 0x77, 0x6F};
+
+        // {2: "two", "one": 1}
+        byte[] second =
+                new byte[]{(byte) 0xA2, 0x02, 0x63, 0x74, 0x77, 0x6F, 0x63, 0x6F, 0x6E, 0x65, 0x01};
+
+        assertArrayEquals(Util.canonicalizeCbor(first), Util.canonicalizeCbor(second));
+    }
+
+    @Test
+    public void cborEncodeDecodeSingle() throws Exception {
+        List<DataItem> items = new CborBuilder()
+                .addMap().put(1,"one").put("one", 1).end()
+                .addArray().add(42).add(true).addMap().end().end()
+                .add("STRING")
+                .build();
+        for (DataItem item: items) {
+            assertEquals(item, Util.cborToDataItem(Util.cborEncode(item)));
+        }
+    }
+
+    @Test
+    public void cborEncodeDecodeBoolean() {
+        assertEquals(true, Util.cborDecodeBoolean(Util.cborEncodeBoolean(true)));
+        assertEquals(false, Util.cborDecodeBoolean(Util.cborEncodeBoolean(false)));
+    }
+
+    @Test
+    public void cborEncodeDecodeString() {
+        assertEquals("foo bar", Util.cborDecodeString(Util.cborEncodeString("foo bar")));
+    }
+
+    @Test
+    public void cborEncodeDecodeBytestring() {
+        byte[] bits = new byte[256];
+        for (int i = 0; i < bits.length; ++i) {
+            bits[i] = (byte)i;
+        }
+        assertArrayEquals(bits, Util.cborDecodeBytestring(Util.cborEncodeBytestring(bits)));
+    }
+
+    @Test
+    public void cborEncodeDecodeInt() {
+        assertEquals(0, Util.cborDecodeInt(Util.cborEncodeInt(0)));
+        assertEquals(Integer.MAX_VALUE, Util.cborDecodeInt(Util.cborEncodeInt(Integer.MAX_VALUE)));
+        assertEquals(Integer.MIN_VALUE, Util.cborDecodeInt(Util.cborEncodeInt(Integer.MIN_VALUE)));
+    }
+
+    @Test
+    public void prependSemanticTagForEncodedCbor() throws Exception {
+        byte[] inputBytes = new byte[] {1, 2, 3, 4};
+        byte[] encodedInput = Util.cborEncodeBytestring(inputBytes);
+        byte[] encodedWithTag = Util.prependSemanticTagForEncodedCbor(encodedInput);
+
+        ByteString decodedWithTag = (ByteString)Util.cborToDataItem(encodedWithTag);
+        assertEquals(decodedWithTag.getTag().getValue(), 24);  // RFC 8949 defines 24
+
+        byte[] decodedBytes = Util.cborDecodeBytestring(decodedWithTag.getBytes());
+        assertArrayEquals(inputBytes, decodedBytes);
+    }
+
+    private KeyPair coseGenerateKeyPair() throws Exception {
+        KeyPairGenerator kpg = KeyPairGenerator.getInstance(
+            KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
+        KeyGenParameterSpec.Builder builder =
+                new KeyGenParameterSpec.Builder(
+                    "coseTestKeyPair",
+                    KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY)
+                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512);
+        kpg.initialize(builder.build());
+        return kpg.generateKeyPair();
+    }
+
+    @Test
+    public void coseSignAndVerify() throws Exception {
+        KeyPair keyPair = coseGenerateKeyPair();
+        byte[] data = new byte[] {0x10, 0x11, 0x12, 0x13};
+        byte[] detachedContent = new byte[] {};
+        byte[] sig = Util.coseSign1Sign(keyPair.getPrivate(), data, detachedContent, null);
+        assertTrue(Util.coseSign1CheckSignature(sig, detachedContent, keyPair.getPublic()));
+        assertArrayEquals(data, Util.coseSign1GetData(sig));
+        assertEquals(new ArrayList() {}, Util.coseSign1GetX5Chain(sig));
+    }
+
+    @Test
+    public void coseSignAndVerifyDetachedContent() throws Exception {
+        KeyPair keyPair = coseGenerateKeyPair();
+        byte[] data = new byte[] {};
+        byte[] detachedContent = new byte[] {0x20, 0x21, 0x22, 0x23, 0x24};
+        byte[] sig = Util.coseSign1Sign(keyPair.getPrivate(), data, detachedContent, null);
+        assertTrue(Util.coseSign1CheckSignature(sig, detachedContent, keyPair.getPublic()));
+        assertArrayEquals(data, Util.coseSign1GetData(sig));
+        assertEquals(new ArrayList() {}, Util.coseSign1GetX5Chain(sig));
+    }
+
+    @Test
+    public void coseSignAndVerifySingleCertificate() throws Exception {
+        KeyPair keyPair = coseGenerateKeyPair();
+        byte[] data = new byte[] {};
+        byte[] detachedContent = new byte[] {0x20, 0x21, 0x22, 0x23, 0x24};
+        ArrayList<X509Certificate> certs = new ArrayList() {};
+        certs.add(Util.signPublicKeyWithPrivateKey("coseTestKeyPair", "coseTestKeyPair"));
+        byte[] sig = Util.coseSign1Sign(keyPair.getPrivate(), data, detachedContent, certs);
+        assertTrue(Util.coseSign1CheckSignature(sig, detachedContent, keyPair.getPublic()));
+        assertArrayEquals(data, Util.coseSign1GetData(sig));
+        assertEquals(certs, Util.coseSign1GetX5Chain(sig));
+    }
+
+    @Test
+    public void coseSignAndVerifyMultipleCertificates() throws Exception {
+        KeyPair keyPair = coseGenerateKeyPair();
+        byte[] data = new byte[] {};
+        byte[] detachedContent = new byte[] {0x20, 0x21, 0x22, 0x23, 0x24};
+        ArrayList<X509Certificate> certs = new ArrayList() {};
+        certs.add(Util.signPublicKeyWithPrivateKey("coseTestKeyPair", "coseTestKeyPair"));
+        certs.add(Util.signPublicKeyWithPrivateKey("coseTestKeyPair", "coseTestKeyPair"));
+        certs.add(Util.signPublicKeyWithPrivateKey("coseTestKeyPair", "coseTestKeyPair"));
+        byte[] sig = Util.coseSign1Sign(keyPair.getPrivate(), data, detachedContent, certs);
+        assertTrue(Util.coseSign1CheckSignature(sig, detachedContent, keyPair.getPublic()));
+        assertArrayEquals(data, Util.coseSign1GetData(sig));
+        assertEquals(certs, Util.coseSign1GetX5Chain(sig));
+    }
+
+    @Test
+    public void coseMac0() throws Exception {
+        SecretKey secretKey = new SecretKeySpec(new byte[32], "");
+        byte[] data = new byte[] {0x10, 0x11, 0x12, 0x13};
+        byte[] detachedContent = new byte[] {};
+        byte[] mac = Util.coseMac0(secretKey, data, detachedContent);
+        assertEquals("[\n"
+                + "  [0xa1, 0x01, 0x05],\n"
+                + "  {},\n"
+                + "  [0x10, 0x11, 0x12, 0x13],\n"
+                + "  [0x6c, 0xec, 0xb5, 0x6a, 0xc9, 0x5c, 0xae, 0x3b, 0x41, 0x13, 0xde, 0xa4, "
+                + "0xd8, 0x86, 0x5c, 0x28, 0x2c, 0xd5, 0xa5, 0x13, 0xff, 0x3b, 0xd1, 0xde, 0x70, "
+                + "0x5e, 0xbb, 0xe2, 0x2d, 0x42, 0xbe, 0x53]\n"
+                + "]", Util.cborPrettyPrint(mac));
+    }
+
+    @Test
+    public void coseMac0DetachedContent() throws Exception {
+        SecretKey secretKey = new SecretKeySpec(new byte[32], "");
+        byte[] data = new byte[] {};
+        byte[] detachedContent = new byte[] {0x10, 0x11, 0x12, 0x13};
+        byte[] mac = Util.coseMac0(secretKey, data, detachedContent);
+        // Same HMAC as in coseMac0 test, only difference is that payload is null.
+        assertEquals("[\n"
+                + "  [0xa1, 0x01, 0x05],\n"
+                + "  {},\n"
+                + "  null,\n"
+                + "  [0x6c, 0xec, 0xb5, 0x6a, 0xc9, 0x5c, 0xae, 0x3b, 0x41, 0x13, 0xde, 0xa4, "
+                + "0xd8, 0x86, 0x5c, 0x28, 0x2c, 0xd5, 0xa5, 0x13, 0xff, 0x3b, 0xd1, 0xde, 0x70, "
+                + "0x5e, 0xbb, 0xe2, 0x2d, 0x42, 0xbe, 0x53]\n"
+                + "]", Util.cborPrettyPrint(mac));
+    }
+
+    @Test
+    public void replaceLineTest() {
+        assertEquals("foo",
+                Util.replaceLine("Hello World", 0, "foo"));
+        assertEquals("foo\n",
+                Util.replaceLine("Hello World\n", 0, "foo"));
+        assertEquals("Hello World",
+                Util.replaceLine("Hello World", 1, "foo"));
+        assertEquals("Hello World\n",
+                Util.replaceLine("Hello World\n", 1, "foo"));
+        assertEquals("foo\ntwo\nthree",
+                Util.replaceLine("one\ntwo\nthree", 0, "foo"));
+        assertEquals("one\nfoo\nthree",
+                Util.replaceLine("one\ntwo\nthree", 1, "foo"));
+        assertEquals("one\ntwo\nfoo",
+                Util.replaceLine("one\ntwo\nthree", 2, "foo"));
+        assertEquals("one\ntwo\nfoo",
+                Util.replaceLine("one\ntwo\nthree", -1, "foo"));
+        assertEquals("one\ntwo\nthree\nfoo",
+                Util.replaceLine("one\ntwo\nthree\nfour", -1, "foo"));
+        assertEquals("one\ntwo\nfoo\nfour",
+                Util.replaceLine("one\ntwo\nthree\nfour", -2, "foo"));
+    }
+
+}
diff --git a/keystore-engine/keystore2_engine.cpp b/keystore-engine/keystore2_engine.cpp
index 69d2ca6..ee550ca 100644
--- a/keystore-engine/keystore2_engine.cpp
+++ b/keystore-engine/keystore2_engine.cpp
@@ -357,7 +357,7 @@
 
     // If the key_id starts with the grant id prefix, we parse the following string as numeric
     // grant id. We can then use the grant domain without alias to load the designated key.
-    if (alias.find(keystore2_grant_id_prefix) == 0) {
+    if (android::base::StartsWith(alias, keystore2_grant_id_prefix)) {
         std::stringstream s(alias.substr(keystore2_grant_id_prefix.size()));
         s >> std::hex >> reinterpret_cast<uint64_t&>(descriptor.nspace);
         descriptor.domain = ks2::Domain::GRANT;
diff --git a/keystore2/Android.bp b/keystore2/Android.bp
index a7aa8fc..7f1d15d 100644
--- a/keystore2/Android.bp
+++ b/keystore2/Android.bp
@@ -74,8 +74,32 @@
     crate_name: "keystore2_test_utils",
     srcs: ["test_utils/lib.rs"],
     rustlibs: [
+        "libkeystore2_selinux",
         "liblog_rust",
+        "libnix",
         "librand",
+        "libserde",
+        "libserde_cbor",
+    ],
+}
+
+rust_test {
+    name: "keystore2_test_utils_test",
+    srcs: ["test_utils/lib.rs"],
+    test_suites: ["general-tests"],
+    // TODO Remove custom test_config and enable the following two lines when
+    //      b/200602232 was resolved.
+    // require_root: true,
+    // auto_gen_config: true,
+    test_config: "test_utils/AndroidTest.xml",
+    compile_multilib: "first",
+    rustlibs: [
+        "libkeystore2_selinux",
+        "liblog_rust",
+        "libnix",
+        "librand",
+        "libserde",
+        "libserde_cbor",
     ],
 }
 
@@ -106,6 +130,7 @@
         "libkeystore2",
         "liblog_rust",
         "liblegacykeystore-rust",
+        "librusqlite",
     ],
     init_rc: ["keystore2.rc"],
 
diff --git a/keystore2/TEST_MAPPING b/keystore2/TEST_MAPPING
index 16b6f85..127ff1e 100644
--- a/keystore2/TEST_MAPPING
+++ b/keystore2/TEST_MAPPING
@@ -10,6 +10,9 @@
       "name": "keystore2_test"
     },
     {
+      "name": "keystore2_test_utils_test"
+    },
+    {
       "name": "CtsIdentityTestCases"
     }
   ]
diff --git a/keystore2/selinux/src/lib.rs b/keystore2/selinux/src/lib.rs
index cf6dfd3..902e9a4 100644
--- a/keystore2/selinux/src/lib.rs
+++ b/keystore2/selinux/src/lib.rs
@@ -321,6 +321,18 @@
     }
 }
 
+/// Safe wrapper around setcon.
+pub fn setcon(target: &CStr) -> std::io::Result<()> {
+    // SAFETY: `setcon` takes a const char* and only performs read accesses on it
+    // using strdup and strcmp. `setcon` does not retain a pointer to `target`
+    // and `target` outlives the call to `setcon`.
+    if unsafe { selinux::setcon(target.as_ptr()) } != 0 {
+        Err(std::io::Error::last_os_error())
+    } else {
+        Ok(())
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/keystore2/src/enforcements.rs b/keystore2/src/enforcements.rs
index e9a58f9..997e739 100644
--- a/keystore2/src/enforcements.rs
+++ b/keystore2/src/enforcements.rs
@@ -837,8 +837,12 @@
                         .context("In get_auth_tokens: No auth token found.");
                 }
             } else {
-                return Err(AuthzError::Rc(AuthzResponseCode::NO_AUTH_TOKEN_FOUND))
-                    .context("In get_auth_tokens: Passed-in auth token max age is zero.");
+                return Err(AuthzError::Rc(AuthzResponseCode::NO_AUTH_TOKEN_FOUND)).context(
+                    concat!(
+                        "In get_auth_tokens: No auth token found for ",
+                        "the given challenge and passed-in auth token max age is zero."
+                    ),
+                );
             }
         };
         // Wait and obtain the timestamp token from secure clock service.
diff --git a/keystore2/src/keystore2_main.rs b/keystore2/src/keystore2_main.rs
index f1f01c6..abab4b6 100644
--- a/keystore2/src/keystore2_main.rs
+++ b/keystore2/src/keystore2_main.rs
@@ -25,7 +25,8 @@
 use keystore2::{authorization::AuthorizationManager, id_rotation::IdRotationState};
 use legacykeystore::LegacyKeystore;
 use log::{error, info};
-use std::{panic, path::Path, sync::mpsc::channel};
+use rusqlite::trace as sqlite_trace;
+use std::{os::raw::c_int, panic, path::Path, sync::mpsc::channel};
 
 static KS2_SERVICE_NAME: &str = "android.system.keystore2.IKeystoreService/default";
 static APC_SERVICE_NAME: &str = "android.security.apc";
@@ -52,6 +53,14 @@
     let mut args = std::env::args();
     args.next().expect("That's odd. How is there not even a first argument?");
 
+    // This must happen early before any other sqlite operations.
+    log::info!("Setting up sqlite logging for keystore2");
+    fn sqlite_log_handler(err: c_int, message: &str) {
+        log::error!("[SQLITE3] {}: {}", err, message);
+    }
+    unsafe { sqlite_trace::config_log(Some(sqlite_log_handler)) }
+        .expect("Error setting sqlite log callback.");
+
     // Write/update keystore.crash_count system property.
     metrics_store::update_keystore_crash_sysprop();
 
diff --git a/keystore2/test_utils/AndroidTest.xml b/keystore2/test_utils/AndroidTest.xml
new file mode 100644
index 0000000..24e277a
--- /dev/null
+++ b/keystore2/test_utils/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+
+<configuration description="Config to run keystore2_test_utils_test device tests.">
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer" />
+
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option
+            name="push"
+            value="keystore2_test_utils_test->/data/local/tmp/keystore2_test_utils_test"
+        />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.rust.RustBinaryTest" >
+        <option name="test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="keystore2_test_utils_test" />
+    </test>
+</configuration>
\ No newline at end of file
diff --git a/keystore2/test_utils/lib.rs b/keystore2/test_utils/lib.rs
index 627af20..a355544 100644
--- a/keystore2/test_utils/lib.rs
+++ b/keystore2/test_utils/lib.rs
@@ -19,6 +19,8 @@
 use std::path::{Path, PathBuf};
 use std::{env::temp_dir, ops::Deref};
 
+pub mod run_as;
+
 /// Represents the lifecycle of a temporary directory for testing.
 #[derive(Debug)]
 pub struct TempDir {
diff --git a/keystore2/test_utils/run_as.rs b/keystore2/test_utils/run_as.rs
new file mode 100644
index 0000000..d42303d
--- /dev/null
+++ b/keystore2/test_utils/run_as.rs
@@ -0,0 +1,191 @@
+// Copyright 2021, 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.
+
+//! This module is intended for testing access control enforcement of services such as keystore2,
+//! by assuming various identities with varying levels of privilege. Consequently, appropriate
+//! privileges are required, or the attempt will fail causing a panic.
+//! The `run_as` module provides the function `run_as`, which takes a UID, GID, an SELinux
+//! context, and a closure. The return type of the closure, which is also the return type of
+//! `run_as`, must implement `serde::Serialize` and `serde::Deserialize`.
+//! `run_as` forks, transitions to the given identity, and executes the closure in the newly
+//! forked process. If the closure returns, i.e., does not panic, the forked process exits with
+//! a status of `0`, and the return value is serialized and sent through a pipe to the parent where
+//! it gets deserialized and returned. The STDIO is not changed and the parent's panic handler
+//! remains unchanged. So if the closure panics, the panic message is printed on the parent's STDERR
+//! and the exit status is set to a non `0` value. The latter causes the parent to panic as well,
+//! and if run in a test context, the test to fail.
+
+use keystore2_selinux as selinux;
+use nix::sys::wait::{waitpid, WaitStatus};
+use nix::unistd::{
+    close, fork, pipe as nix_pipe, read as nix_read, setgid, setuid, write as nix_write,
+    ForkResult, Gid, Uid,
+};
+use serde::{de::DeserializeOwned, Serialize};
+use std::os::unix::io::RawFd;
+
+fn transition(se_context: selinux::Context, uid: Uid, gid: Gid) {
+    setgid(gid).expect("Failed to set GID. This test might need more privileges.");
+    setuid(uid).expect("Failed to set UID. This test might need more privileges.");
+
+    selinux::setcon(&se_context)
+        .expect("Failed to set SELinux context. This test might need more privileges.");
+}
+
+/// PipeReader is a simple wrapper around raw pipe file descriptors.
+/// It takes ownership of the file descriptor and closes it on drop. It provides `read_all`, which
+/// reads from the pipe into an expending vector, until no more data can be read.
+struct PipeReader(RawFd);
+
+impl PipeReader {
+    pub fn read_all(&self) -> Result<Vec<u8>, nix::Error> {
+        let mut buffer = [0u8; 128];
+        let mut result = Vec::<u8>::new();
+        loop {
+            let bytes = nix_read(self.0, &mut buffer)?;
+            if bytes == 0 {
+                return Ok(result);
+            }
+            result.extend_from_slice(&buffer[0..bytes]);
+        }
+    }
+}
+
+impl Drop for PipeReader {
+    fn drop(&mut self) {
+        close(self.0).expect("Failed to close reader pipe fd.");
+    }
+}
+
+/// PipeWriter is a simple wrapper around raw pipe file descriptors.
+/// It takes ownership of the file descriptor and closes it on drop. It provides `write`, which
+/// writes the given buffer into the pipe, returning the number of bytes written.
+struct PipeWriter(RawFd);
+
+impl PipeWriter {
+    pub fn write(&self, data: &[u8]) -> Result<usize, nix::Error> {
+        nix_write(self.0, data)
+    }
+}
+
+impl Drop for PipeWriter {
+    fn drop(&mut self) {
+        close(self.0).expect("Failed to close writer pipe fd.");
+    }
+}
+
+fn pipe() -> Result<(PipeReader, PipeWriter), nix::Error> {
+    let (read_fd, write_fd) = nix_pipe()?;
+    Ok((PipeReader(read_fd), PipeWriter(write_fd)))
+}
+
+/// Run the given closure in a new process running with the new identity given as
+/// `uid`, `gid`, and `se_context`.
+pub fn run_as<F, R>(se_context: &str, uid: Uid, gid: Gid, f: F) -> R
+where
+    R: Serialize + DeserializeOwned,
+    F: 'static + Send + FnOnce() -> R,
+{
+    let se_context =
+        selinux::Context::new(se_context).expect("Unable to construct selinux::Context.");
+    let (reader, writer) = pipe().expect("Failed to create pipe.");
+
+    match unsafe { fork() } {
+        Ok(ForkResult::Parent { child, .. }) => {
+            drop(writer);
+            let status = waitpid(child, None).expect("Failed while waiting for child.");
+            if let WaitStatus::Exited(_, 0) = status {
+                // Child exited successfully.
+                // Read the result from the pipe.
+                let serialized_result =
+                    reader.read_all().expect("Failed to read result from child.");
+
+                // Deserialize the result and return it.
+                serde_cbor::from_slice(&serialized_result).expect("Failed to deserialize result.")
+            } else {
+                panic!("Child did not exit as expected {:?}", status);
+            }
+        }
+        Ok(ForkResult::Child) => {
+            // This will panic on error or insufficient privileges.
+            transition(se_context, uid, gid);
+
+            // Run the closure.
+            let result = f();
+
+            // Serialize the result of the closure.
+            let vec = serde_cbor::to_vec(&result).expect("Result serialization failed");
+
+            // Send the result to the parent using the pipe.
+            writer.write(&vec).expect("Failed to send serialized result to parent.");
+
+            // Set exit status to `0`.
+            std::process::exit(0);
+        }
+        Err(errno) => {
+            panic!("Failed to fork: {:?}", errno);
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use keystore2_selinux as selinux;
+    use nix::unistd::{getgid, getuid};
+    use serde::{Deserialize, Serialize};
+
+    /// This test checks that the closure does not produce an exit status of `0` when run inside a
+    /// test and the closure panics. This would mask test failures as success.
+    #[test]
+    #[should_panic]
+    fn test_run_as_panics_on_closure_panic() {
+        run_as(selinux::getcon().unwrap().to_str().unwrap(), getuid(), getgid(), || {
+            panic!("Closure must panic.")
+        });
+    }
+
+    static TARGET_UID: Uid = Uid::from_raw(10020);
+    static TARGET_GID: Gid = Gid::from_raw(10020);
+    static TARGET_CTX: &str = "u:r:untrusted_app:s0:c91,c256,c10,c20";
+
+    /// Tests that the closure is running as the target identity.
+    #[test]
+    fn test_transition_to_untrusted_app() {
+        run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || {
+            assert_eq!(TARGET_UID, getuid());
+            assert_eq!(TARGET_GID, getgid());
+            assert_eq!(TARGET_CTX, selinux::getcon().unwrap().to_str().unwrap());
+        });
+    }
+
+    #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+    struct SomeResult {
+        a: u32,
+        b: u64,
+        c: String,
+    }
+
+    #[test]
+    fn test_serialized_result() {
+        let test_result = SomeResult {
+            a: 5,
+            b: 0xffffffffffffffff,
+            c: "supercalifragilisticexpialidocious".to_owned(),
+        };
+        let test_result_clone = test_result.clone();
+        let result = run_as(TARGET_CTX, TARGET_UID, TARGET_GID, || test_result_clone);
+        assert_eq!(test_result, result);
+    }
+}