Add utilities for ISO 18013-5 (mdl)
These are mobile driving license helpers and constants for working with
the mDL application protocol.
Change-Id: I51a8845fad170cd596b92103a409ca94fc98d69c
Test: CtsIdentityTestCases
Test: IdCredSupportTests
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
index cd74059..b74efb7 100644
--- a/identity/util/src/java/com/android/security/identity/internal/Util.java
+++ b/identity/util/src/java/com/android/security/identity/internal/Util.java
@@ -977,12 +977,18 @@
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 {
- ByteString taggedBytestring = new ByteString(encodedCbor);
- taggedBytestring.setTag(CBOR_SEMANTIC_TAG_ENCODED_CBOR);
- new CborEncoder(baos).encode(taggedBytestring);
+ new CborEncoder(baos).encode(buildCborTaggedByteString(encodedCbor));
} catch (CborException e) {
throw new RuntimeException("Error encoding with semantic tag for CBOR encoding", e);
}