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);
+ }
+}