Make CBOR and COSE CTS utilities reusable

We need these utilities for tools, and they may prove handy for
production code. Move them into system/security and flesh out the
unit tests.

Test: IdCredSupportTests
Change-Id: I18dd909e46aec5e315adb4358dc23088d70fa899
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/Util.java b/identity/util/src/java/com/android/security/identity/internal/Util.java
new file mode 100644
index 0000000..cd74059
--- /dev/null
+++ b/identity/util/src/java/com/android/security/identity/internal/Util.java
@@ -0,0 +1,1308 @@
+/*
+ * 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);
+    }
+
+    public static byte[] prependSemanticTagForEncodedCbor(byte[] encodedCbor) {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try {
+            ByteString taggedBytestring = new ByteString(encodedCbor);
+            taggedBytestring.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
+            new CborEncoder(baos).encode(taggedBytestring);
+        } 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"));
+    }
+
+}