Skip to content

Commit

Permalink
Performance improvements and new sample apps.
Browse files Browse the repository at this point in the history
This PR introduces two sample apps - `preconsent-mdl` and
`age-verifier-mdl` - to help with pinpointing performance issues with
the libraries.

This helped identify two problems making both the reader and the
holder feel sluggish.

- Calling `BouncyCastle()` is extremely expensive and we were doing
  this in a number of utility functions, including `Util.coseKeyDecode()`.
  This is fixed by using variants that pass `BouncyCastle.PROVIDER_NAME` instead.
  This also requires fixing up apps and test cases to manually register
  BouncyCastle at startup.

- Determining the EC curve for a `PublicKey` is very expensive and
  also potentially fragile. Instead, modify the whole library stack to
  pass the curve along the public key. This is possible because we
  always know the curve, either if we created the key ourselves or if
  we received it from the reader or issuer (through COSE_Key).

There's still room for optimization but these two fixes already help a
lot and the two new samples makes it easier to identify and fix other
bottlenecks.

Other changes:

- Use version catalogs in identity and identity-android.

- Introduce DataTransportUdp as a super-low-latency data transfer
  method. The goal of this is to be able to pinpoint bottlenecks
  in non-data-transfer related code. For example, prior to the
  performance fixes mentioned above, it would occasionally take
  almost 1000ms to process DeviceEngagement and send encrypted
  DeviceRequest.

- Remove reportConnectionMethodReady() as it's not needed for
  any of the data transfer methods in ISO 18013-5. For our two
  proprietary data transfer methods (DataTransportTcp and
  DataTransportUdp), just have `connect()` block on a call to
  the kernel to get a port number assigned.

- Have `Credential.findAuthenticationKey()` also take the domain.
  This was really just an oversight when we added started tying
  authentication keys to domains.

- Add `onHandoverSelectMessageSent()` callback to `NfcEngagementHelper`.
  This gives the mdoc application an opportunity to signal to the
  user that it's now safe to remove the device from the NFC field,
  for example by vibration.

Fixes Issue #387.

Test: Manually tested and all unit tests pass.
Signed-off-by: David Zeuthen <[email protected]>
  • Loading branch information
davidz25 committed Dec 13, 2023
1 parent 65ea519 commit eb94948
Show file tree
Hide file tree
Showing 124 changed files with 4,344 additions and 632 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ reachable on the left side bar of the Android Studio, or by selecting: _View ->
Inside the `Build Variants` panel, at the `appholder` row, the desired flavor can be chosen. Once a
flavor is selected, by running the app it will install it on the target device/emulator.

## Sample Applications

The `samples/` directory contain a number of sample applications, intended primarily
to show certain library features or assess performance or correctness. The following
samples are included

- `preconsent-mdl` - Simple mDL application without user consent authentication.
- The main purpose of this sample is to assess performance of our libraries, Android,
the device it's being run on, and the mDL reader requesting the mDL.
- The application allows the user to easily configure which kind of data transfer
method to use, including an idealized near-zero latency method (`DataTransportUdp`)
to help pinpoint potential performance bottlenecks not related to data transfer.
- `age-verifier-mdl` - a simple mDL reader for age attestations.
- This application is just requesting the `age_over_21` and `portrait`. It's intended
to be used with the `preconsent-mdl` sample for performance evaluation.

## ISO 18013-7 Reader Website

The `wwwverifier` module contains the source code for a website acting as an
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ class QrCommunicationSetup(
context,
deviceRetrievalHelperListener,
context.mainExecutor(),
eDeviceKeyPair
eDeviceKeyPair,
settings.getEphemeralKeyCurveOption()
)
builder.useForwardEngagement(
transport,
Expand Down Expand Up @@ -97,6 +98,7 @@ class QrCommunicationSetup(
qrEngagement = QrEngagementHelper.Builder(
context,
eDeviceKeyPair.public,
settings.getEphemeralKeyCurveOption(),
connectionSetup.getConnectionOptions(),
qrEngagementListener,
context.mainExecutor()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class ReverseQrCommunicationSetup(
context,
presentationListener,
context.mainExecutor(),
eDeviceKeyPair
eDeviceKeyPair,
settings.getEphemeralKeyCurveOption()
)
builder.useReverseEngagement(transport, encodedReaderEngagement, origins)
presentation = builder.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ class TransferManager private constructor(private val context: Context) {
if (authKey != null) {
authKeyToUse = authKey
} else {
authKeyToUse = credential.findAuthenticationKey(Timestamp.now())
authKeyToUse = credential.findAuthenticationKey(ProvisioningUtil.AUTH_KEY_DOMAIN, Timestamp.now())
?: throw IllegalStateException("No auth key available")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class NfcEngagementHandler : HostApduService() {
log("Engagement Listener: Two Way Engagement Detected.")
}

override fun onHandoverSelectMessageSent() {
log("Engagement Listener: Handover Select Message Sent.")
}

override fun onDeviceConnecting() {
log("Engagement Listener: Device Connecting. Launching Transfer Screen")
val pendingIntent = NavDeepLinkBuilder(applicationContext)
Expand All @@ -75,7 +79,8 @@ class NfcEngagementHandler : HostApduService() {
applicationContext,
presentationListener,
applicationContext.mainExecutor(),
eDeviceKeyPair
eDeviceKeyPair,
settings.getEphemeralKeyCurveOption()
)
builder.useForwardEngagement(
transport,
Expand Down Expand Up @@ -128,6 +133,7 @@ class NfcEngagementHandler : HostApduService() {
val builder = NfcEngagementHelper.Builder(
applicationContext,
eDeviceKeyPair.public,
settings.getEphemeralKeyCurveOption(),
connectionSetup.getConnectionOptions(),
nfcEngagementListener,
applicationContext.mainExecutor()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ class ProvisioningUtil private constructor(
val msoGenerator = MobileSecurityObjectGenerator(
"SHA-256",
docType,
pendingAuthKey.attestation.first().publicKey
pendingAuthKey.attestation.first().publicKey,
settings.ecCurve
)
msoGenerator.setValidityInfo(now, validFrom, validUntil, null)

Expand Down Expand Up @@ -237,7 +238,7 @@ class ProvisioningUtil private constructor(

companion object {

private const val AUTH_KEY_DOMAIN = "mdoc/MSO"
const val AUTH_KEY_DOMAIN = "mdoc/MSO"
private const val USER_VISIBLE_NAME = "userVisibleName"
private const val DOCUMENT_TYPE = "documentType"
private const val DATE_PROVISIONED = "dateProvisioned"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ class ShowDocumentFragment : Fragment() {
// Just show the SHA-1 of DeviceKey since all we're interested in here is whether
// we saw the same key earlier.
sb.append("<h6>DeviceKey</h6>")
sb.append("${getFormattedCheck(true)}Curve: <b>${curveNameFor(Util.getCurve(doc.deviceKey))}</b><br>")
sb.append("${getFormattedCheck(true)}Curve: <b>${curveNameFor(doc.deviceKeyCurve)}</b><br>")
val deviceKeySha1 = FormatUtil.encodeToString(
MessageDigest.getInstance("SHA-1").digest(doc.deviceKey.encoded)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public boolean validateCertificationTrustPath(List<X509Certificate> certificatio
prevCert.verify(caCert.getPublicKey());
} catch (InvalidKeyException e) {
// Try to decode certificate using BouncyCastleProvider
CertificateFactory factory = CertificateFactory.getInstance("X509", new BouncyCastleProvider());
CertificateFactory factory = CertificateFactory.getInstance("X509", BouncyCastleProvider.PROVIDER_NAME);
X509Certificate prevCertBC = (X509Certificate) factory.generateCertificate(
new ByteArrayInputStream(prevCert.getEncoded()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ class TransferManager private constructor(private val context: Context) {
}

fun getMdocSessionEncryptionCurve(): Int {
return Util.getCurve(verification!!.eReaderKeyPair.public)
return verification!!.eReaderKeyCurve
}

fun getTapToEngagementDurationMillis(): Long {
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
espresso-core = "3.5.1"
junit-jupiter = "5.10.0"
truth = "1.1.5"
navigation-compose = "2.7.5"

[libraries]
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
Expand Down Expand Up @@ -85,6 +86,7 @@
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit-jupiter" }
kotlinx-coroutine-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines-version" }
truth = { module = "com.google.truth:truth", version.ref = "truth" }
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation-compose" }

[bundles]
androidx-core = ["androidx-core-ktx", "androidx-appcompat", "androidx-material", "androidx-contraint-layout", "androidx-fragment-ktx", "androidx-legacy-v4", "androidx-preference-ktx", "androidx-work"]
Expand Down
24 changes: 12 additions & 12 deletions identity-android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,20 @@ android {

dependencies {
implementation project(':identity')
implementation("androidx.annotation:annotation:1.5.0")
implementation "androidx.biometric:biometric:1.2.0-alpha05"
implementation "co.nstant.in:cbor:0.9"
implementation "org.bouncycastle:bcprov-jdk15on:1.67"
implementation("org.bouncycastle:bcpkix-jdk15on:1.67")
implementation "com.android.volley:volley:1.2.1"
implementation(libs.androidx.annotation)
implementation libs.androidx.biometrics
implementation libs.cbor
implementation libs.bouncy.castle.bcprov
implementation(libs.bouncy.castle.bcpkix)
implementation libs.volley

testImplementation "androidx.test.espresso:espresso-core:3.4.0"
testImplementation "androidx.test.ext:junit:1.1.3"
testImplementation "junit:junit:4.13.2"
testImplementation "org.bouncycastle:bcprov-jdk15on:1.67"
testImplementation libs.androidx.test.espresso
testImplementation libs.androidx.test.ext.junit
testImplementation libs.bouncy.castle.bcprov
testImplementation libs.bundles.unit.testing

androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
androidTestImplementation libs.androidx.test.ext.junit
androidTestImplementation libs.androidx.test.espresso
}

tasks.withType(Test) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,26 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;

import com.android.identity.android.securearea.AndroidKeystoreSecureArea;
import com.android.identity.android.storage.AndroidStorageEngine;
import com.android.identity.internal.Util;
import com.android.identity.securearea.SecureArea;
import com.android.identity.securearea.SecureAreaRepository;
import com.android.identity.util.Constants;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
Expand All @@ -70,6 +77,12 @@
public class DynamicAuthTest {
private static final String TAG = "DynamicAuthTest";

@Before
public void setup() {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
Security.addProvider(new BouncyCastleProvider());
}

@SuppressWarnings("deprecation")
@Test
public void checkAuthKey() throws Exception {
Expand Down
Loading

0 comments on commit eb94948

Please sign in to comment.