Skip to content

Commit

Permalink
Add support for per-MSO data elements.
Browse files Browse the repository at this point in the history
Our current model involves sending all PII to the device and then
a bunch of MSOs + associated data (StaticAuthData) where the
latter doesn't include any PII at all. Add the ability for
StaticAuthData to also include PII meaning the issuer has the
option to send none, some, or all PII along with each MSO.

Also use this in appholder by embedding the string "MSO <counter>" on
top of the portrait image where <counter> is the count of the MSO
and increasing for every use. In this example, only the portrait
data element is per-MSO.

Fixes #405.

Test: Manually tested.
Test: New unit tests and all unit tests pass.
  • Loading branch information
davidz25 committed Nov 1, 2023
1 parent 2279457 commit 14284bf
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ package com.android.identity.wallet.util

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import com.android.identity.android.securearea.AndroidKeystoreSecureArea
import com.android.identity.android.storage.AndroidStorageEngine
import com.android.identity.credential.Credential
Expand All @@ -13,7 +19,6 @@ import com.android.identity.mdoc.mso.MobileSecurityObjectGenerator
import com.android.identity.mdoc.mso.StaticAuthDataGenerator
import com.android.identity.mdoc.util.MdocUtil
import com.android.identity.securearea.SecureArea
import com.android.identity.securearea.SecureArea.KeyLockedException
import com.android.identity.securearea.SecureArea.KeyPurpose
import com.android.identity.securearea.SecureAreaRepository
import com.android.identity.securearea.SoftwareSecureArea
Expand All @@ -26,8 +31,8 @@ import com.android.identity.wallet.selfsigned.AddSelfSignedScreenState
import com.android.identity.wallet.selfsigned.ProvisionInfo
import com.android.identity.wallet.util.DocumentData.MICOV_DOCTYPE
import com.android.identity.wallet.util.DocumentData.MVR_DOCTYPE
import java.io.ByteArrayOutputStream
import java.io.File
import java.security.KeyPair
import java.security.PrivateKey
import java.security.cert.X509Certificate
import java.time.Instant
Expand Down Expand Up @@ -165,6 +170,7 @@ class ProvisioningUtil private constructor(
val timeSigned = Timestamp.now()
val timeValidityBegin = Timestamp.ofEpochMilli(nowMillis)
val timeValidityEnd = validityInDays.toTimestampFromNow()

for (pendingAuthKey in credential.pendingAuthenticationKeys) {
val msoGenerator = MobileSecurityObjectGenerator(
"SHA-256",
Expand All @@ -173,11 +179,29 @@ class ProvisioningUtil private constructor(
)
msoGenerator.setValidityInfo(timeSigned, timeValidityBegin, timeValidityEnd, null)

// For mDLs, override the portrait with AuthenticationKeyCounter on top
//
var dataElementExceptions: Map<String, List<String>>? = null
var dataElementOverrides: Map<String, Map<String, ByteArray>>? = null
if (documentType.equals("org.iso.18013.5.1.mDL")) {
val portrait = credential.nameSpacedData.getDataElementByteString(
"org.iso.18013.5.1", "portrait")
val portrait_override = overridePortrait(portrait,
pendingAuthKey.authenticationKeyCounter)

dataElementExceptions =
mapOf("org.iso.18013.5.1" to listOf("given_name", "portrait"))
dataElementOverrides =
mapOf("org.iso.18013.5.1" to mapOf(
"portrait" to Util.cborEncodeBytestring(portrait_override)))
}

val deterministicRandomProvider = Random(42)
val issuerNameSpaces = MdocUtil.generateIssuerNameSpaces(
credential.nameSpacedData,
deterministicRandomProvider,
16
16,
dataElementOverrides
)

for (nameSpaceName in issuerNameSpaces.keys) {
Expand Down Expand Up @@ -217,7 +241,7 @@ class ProvisioningUtil private constructor(
)

val issuerProvidedAuthenticationData = StaticAuthDataGenerator(
MdocUtil.stripIssuerNameSpaces(issuerNameSpaces),
MdocUtil.stripIssuerNameSpaces(issuerNameSpaces, dataElementExceptions),
encodedIssuerAuth
).generate()

Expand All @@ -229,6 +253,35 @@ class ProvisioningUtil private constructor(
}
}

// Puts the string "MSO ${counter}" on top of the portrait image.
private fun overridePortrait(encodedPortrait: ByteArray, counter: Number): ByteArray {
val options = BitmapFactory.Options()
options.inMutable = true
val bitmap = BitmapFactory.decodeByteArray(
encodedPortrait,
0,
encodedPortrait.size,
options)

val text = "MSO ${counter}"
val canvas = Canvas(bitmap)
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
paint.setColor(Color.WHITE)
paint.textSize = bitmap.width / 5.0f
paint.setShadowLayer(2.0f, 1.0f, 1.0f, Color.BLACK)
val bounds = Rect()
paint.getTextBounds(text, 0, text.length, bounds)
val x: Float = (bitmap.width - bounds.width()) / 2.0f
val y: Float = (bitmap.height - bounds.height()) / 4.0f
canvas.drawText(text, x, y, paint)

val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, baos)
val encodedModifiedPortrait: ByteArray = baos.toByteArray()

return encodedModifiedPortrait
}

private fun manageKeysFor(credential: Credential): Int {
val mDocAuthOption = credential.applicationData.getString(MDOC_AUTHENTICATION)
val settings = when (credential.credentialSecureArea) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,8 @@ private void migrateAndCheckResults(String credName,
Map<String, List<byte[]>> issuerNameSpaces = MdocUtil.generateIssuerNameSpaces(
nsData,
deterministicRandomProvider,
16);
16,
null);

for (String nameSpaceName : issuerNameSpaces.keySet()) {
Map<Long, byte[]> digests = MdocUtil.calculateDigestsForNameSpace(
Expand Down Expand Up @@ -358,7 +359,7 @@ private void migrateAndCheckResults(String credName,
issuerCertChain));

byte[] issuerProvidedAuthenticationData = new StaticAuthDataGenerator(
MdocUtil.stripIssuerNameSpaces(issuerNameSpaces),
MdocUtil.stripIssuerNameSpaces(issuerNameSpaces, null),
encodedIssuerAuth).generate();

// Now that we have issuer-provided authentication data we certify the authentication key.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,18 @@ public void setNameSpacedData(@NonNull NameSpacedData nameSpacedData) {
return candidate;
}

/**
* Gets the authentication key counter.
*
* <p>This is a number which starts at 0 and is increased by one for every call
* to {@link #createPendingAuthenticationKey(SecureArea.CreateKeySettings, AuthenticationKey)}.
*
* @return the authentication key counter.
*/
public long getAuthenticationKeyCounter() {
return mAuthenticationKeyCounter;
}

/**
* A certified authentication key.
*
Expand All @@ -419,6 +431,7 @@ public static class AuthenticationKey {
private String mSecureAreaName;
private String mReplacementAlias;
private SimpleApplicationData mApplicationData;
private long mAuthenticationKeyCounter;

static AuthenticationKey create(
@NonNull PendingAuthenticationKey pendingAuthenticationKey,
Expand All @@ -434,6 +447,7 @@ static AuthenticationKey create(
ret.mCredential = credential;
ret.mSecureAreaName = pendingAuthenticationKey.mSecureAreaName;
ret.mApplicationData = pendingAuthenticationKey.mApplicationData;
ret.mAuthenticationKeyCounter = pendingAuthenticationKey.mAuthenticationKeyCounter;
return ret;
}

Expand Down Expand Up @@ -473,6 +487,19 @@ public int getUsageCount() {
return mValidUntil;
}

/**
* Gets the authentication key counter.
*
* <p>This is the value of the Credential's Authentication Key Counter
* at the time the pending authentication key for this authentication key
* was created.
*
* @return the authentication key counter.
*/
public long getAuthenticationKeyCounter() {
return mAuthenticationKeyCounter;
}

/**
* Deletes the authentication key.
*
Expand Down Expand Up @@ -549,7 +576,8 @@ DataItem toCbor() {
.put("data", mData)
.put("validFrom", mValidFrom.toEpochMilli())
.put("validUntil", mValidUntil.toEpochMilli())
.put("applicationData", mApplicationData.encodeAsCbor());
.put("applicationData", mApplicationData.encodeAsCbor())
.put("authenticationKeyCounter", mAuthenticationKeyCounter);
if (mReplacementAlias != null) {
mapBuilder.put("replacementAlias", mReplacementAlias);
}
Expand All @@ -576,6 +604,9 @@ static AuthenticationKey fromCbor(@NonNull DataItem dataItem,
ret.mApplicationData = SimpleApplicationData.decodeFromCbor(
((ByteString) applicationDataDataItem).getBytes(),
() -> ret.mCredential.saveCredential());
if (Util.cborMapHasKey(dataItem, "authenticationKeyCounter")) {
ret.mAuthenticationKeyCounter = Util.cborMapExtractNumber(dataItem, "authenticationKeyCounter");
}
return ret;
}

Expand Down Expand Up @@ -640,6 +671,7 @@ public static class PendingAuthenticationKey {
Credential mCredential;
private String mReplacementForAlias;
private SimpleApplicationData mApplicationData;
private long mAuthenticationKeyCounter;

static @NonNull PendingAuthenticationKey create(
@NonNull String alias,
Expand All @@ -660,6 +692,7 @@ public static class PendingAuthenticationKey {
}
ret.mCredential = credential;
ret.mApplicationData = new SimpleApplicationData(() -> ret.mCredential.saveCredential());
ret.mAuthenticationKeyCounter = credential.mAuthenticationKeyCounter;
return ret;
}

Expand Down Expand Up @@ -710,6 +743,18 @@ public static class PendingAuthenticationKey {
return secureArea;
}

/**
* Gets the authentication key counter.
*
* <p>This is the value of the Credential's Authentication Key Counter
* at the time this pending authentication key was created.
*
* @return the authentication key counter.
*/
public long getAuthenticationKeyCounter() {
return mAuthenticationKeyCounter;
}

/**
* Deletes the pending authentication key.
*/
Expand Down Expand Up @@ -754,7 +799,8 @@ public void delete() {
if (mReplacementForAlias != null) {
mapBuilder.put("replacementForAlias", mReplacementForAlias);
}
mapBuilder.put("applicationData", mApplicationData.encodeAsCbor());
mapBuilder.put("applicationData", mApplicationData.encodeAsCbor())
.put("authenticationKeyCounter", mAuthenticationKeyCounter);
return builder.build().get(0);
}

Expand All @@ -774,6 +820,9 @@ static PendingAuthenticationKey fromCbor(@NonNull DataItem dataItem,
ret.mApplicationData = SimpleApplicationData.decodeFromCbor(
((ByteString) applicationDataDataItem).getBytes(),
() -> ret.mCredential.saveCredential());
if (Util.cborMapHasKey(dataItem, "authenticationKeyCounter")) {
ret.mAuthenticationKeyCounter = Util.cborMapExtractNumber(dataItem, "authenticationKeyCounter");
}
return ret;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,19 @@
* "digestID" : uint, ; Digest ID for issuer data auth
* "random" : bstr, ; Random value for issuer data auth
* "elementIdentifier" : DataElementIdentifier, ; Data element identifier
* "elementValue" : NULL ; Placeholder for Data element value
* "elementValue" : DataElementValueOrNull ; Placeholder for Data element value
* }
*
* ; Set to null to use value previously provisioned or non-null
* ; to use a per-MSO value
* ;
* DataElementValueOrNull = null // DataElementValue ; "//" means or in CDDL
*
* ; Defined in ISO 18013-5
* ;
* NameSpace = String
* DataElementIdentifier = String
* DataElementValue = any
* DigestID = uint
* IssuerAuth = COSE_Sign1 ; The payload is MobileSecurityObjectBytes
* </pre>
Expand Down Expand Up @@ -100,19 +106,6 @@ public byte[] generate() {
ArrayBuilder<MapBuilder<CborBuilder>> innerBuilder = outerBuilder.putArray(namespace);

for (byte[] encodedIssuerSignedItemMetadata : mDigestIDMapping.get(namespace)) {
// Ensure that elementValue is NULL to avoid applications or issuers that send
// the raw DataElementValue in the IssuerSignedItem. If we allowed non-NULL
// values, then PII would be exposed that would otherwise be guarded by
// access control checks.
DataItem issuerSignedItemMetadata = Util.cborDecode(Util.cborExtractTaggedCbor(encodedIssuerSignedItemMetadata));
DataItem value = Util.cborMapExtract(issuerSignedItemMetadata, "elementValue");
if (!(value instanceof SimpleValue)
|| ((SimpleValue) value).getSimpleValueType() != SimpleValueType.NULL) {
String name = Util.cborMapExtractString(issuerSignedItemMetadata, "elementIdentifier");
throw new IllegalArgumentException("elementValue for nameSpace " + namespace
+ " elementName " + name + " is not NULL");
}

innerBuilder.add(Util.cborDecode(encodedIssuerSignedItemMetadata));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,6 @@ private void parseDigestIdMapping(DataItem digestIdMapping) {
if (innerKey.getTag().getValue() != 24) {
throw new IllegalArgumentException("Inner key does not have tag 24");
}
// Strictly not necessary but check that elementValue is NULL. This is to
// avoid applications (or issuers) sending the value in issuerSignedMapping
// which is part of staticAuthData.
DataItem issuerSignedItem = Util.cborExtractTaggedAndEncodedCbor(innerKey);
DataItem value = Util.cborMapExtract(issuerSignedItem, "elementValue");
if (!(value instanceof SimpleValue)
|| ((SimpleValue) value).getSimpleValueType() != SimpleValueType.NULL) {
String name = Util.cborMapExtractString(issuerSignedItem, "elementIdentifier");
throw new IllegalArgumentException("elementValue for nameSpace " + namespace
+ " elementName " + name + " is not NULL");
}
innerArray.add(Util.cborEncode(innerKey));
}

Expand Down
Loading

0 comments on commit 14284bf

Please sign in to comment.