diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 19966672d..639a84ea0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,15 +13,15 @@ androidx-test-junit = "1.1.5" compose-plugin = "1.7.0-rc01" faceDetection = "16.1.6" junit = "4.13.2" -kotlin = "2.0.21" +kotlin = "2.1.0" kotlinx-coroutines = "1.8.1" kotlinx-io = "0.4.0" kotlinx-datetime = "0.6.0" kotlinx-serialization = "1.7.0" bouncy-castle = "1.78.1" tink = "1.13.0" -jetbrainsKotlinJvm = "2.0.21" -ksp = "2.0.21-1.0.26" +jetbrainsKotlinJvm = "2.1.0" +ksp = "2.1.0-1.0.29" androidx-biometrics = "1.2.0-alpha05" volley = "1.2.1" cbor = "0.9" @@ -103,6 +103,7 @@ ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "k ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } diff --git a/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudCreateKeySettings.kt b/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudCreateKeySettings.kt index 113309eb4..974d08406 100644 --- a/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudCreateKeySettings.kt +++ b/identity-android-csa/src/main/java/com/android/identity/android/securearea/cloud/CloudCreateKeySettings.kt @@ -2,10 +2,10 @@ package com.android.identity.android.securearea.cloud import android.os.Build import com.android.identity.android.securearea.UserAuthenticationType -import com.android.identity.cbor.DataItem import com.android.identity.crypto.EcCurve import com.android.identity.securearea.CreateKeySettings import com.android.identity.securearea.KeyPurpose +import com.android.identity.securearea.config.SecureAreaConfigurationCloud import kotlinx.datetime.Instant /** @@ -79,25 +79,15 @@ class CloudCreateKeySettings private constructor( * @param configuration configuration from a CBOR map. * @return the builder. */ - fun applyConfiguration(configuration: DataItem) = apply { - var userAutenticationRequired = false - var userAuthenticationTimeoutMillis = 0L - var userAuthenticationTypes = setOf() - for ((key, value) in configuration.asMap) { - when (key.asTstr) { - "purposes" -> setKeyPurposes(KeyPurpose.decodeSet(value.asNumber)) - "curve" -> setEcCurve(EcCurve.fromInt(value.asNumber.toInt())) - "useStrongBox" -> setUseStrongBox(value.asBoolean) - "userAuthenticationRequired" -> userAutenticationRequired = value.asBoolean - "userAuthenticationTimeoutMillis" -> userAuthenticationTimeoutMillis = value.asNumber - "userAuthenticationTypes" -> userAuthenticationTypes = UserAuthenticationType.decodeSet(value.asNumber) - "passphraseRequired" -> setPassphraseRequired(value.asBoolean) - } - } + fun applyConfiguration(configuration: SecureAreaConfigurationCloud) = apply { + setKeyPurposes(KeyPurpose.decodeSet(configuration.purposes)) + setEcCurve(EcCurve.fromInt(configuration.curve)) + setUseStrongBox(configuration.useStrongBox) + setPassphraseRequired(configuration.passphraseRequired) setUserAuthenticationRequired( - userAutenticationRequired, - userAuthenticationTimeoutMillis, - userAuthenticationTypes + configuration.userAuthenticationRequired, + configuration.userAuthenticationTimeoutMillis, + UserAuthenticationType.decodeSet(configuration.userAuthenticationTypes) ) } diff --git a/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreCreateKeySettings.kt b/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreCreateKeySettings.kt index 0008667d2..2b83592d5 100644 --- a/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreCreateKeySettings.kt +++ b/identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreCreateKeySettings.kt @@ -1,10 +1,10 @@ package com.android.identity.android.securearea import android.os.Build -import com.android.identity.cbor.DataItem import com.android.identity.crypto.EcCurve import com.android.identity.securearea.CreateKeySettings import com.android.identity.securearea.KeyPurpose +import com.android.identity.securearea.config.SecureAreaConfigurationAndroidKeystore import kotlinx.datetime.Instant /** @@ -83,24 +83,14 @@ class AndroidKeystoreCreateKeySettings private constructor( * @param configuration configuration from a CBOR map. * @return the builder. */ - fun applyConfiguration(configuration: DataItem) = apply { - var userAutenticationRequired = false - var userAuthenticationTimeoutMillis = 0L - var userAuthenticationTypes = setOf() - for ((key, value) in configuration.asMap) { - when (key.asTstr) { - "purposes" -> setKeyPurposes(KeyPurpose.decodeSet(value.asNumber)) - "curve" -> setEcCurve(EcCurve.fromInt(value.asNumber.toInt())) - "useStrongBox" -> setUseStrongBox(value.asBoolean) - "userAuthenticationRequired" -> userAutenticationRequired = value.asBoolean - "userAuthenticationTimeoutMillis" -> userAuthenticationTimeoutMillis = value.asNumber - "userAuthenticationTypes" -> userAuthenticationTypes = UserAuthenticationType.decodeSet(value.asNumber) - } - } + fun applyConfiguration(configuration: SecureAreaConfigurationAndroidKeystore) = apply { + setKeyPurposes(KeyPurpose.decodeSet(configuration.purposes)) + setEcCurve(EcCurve.fromInt(configuration.curve)) + setUseStrongBox(configuration.useStrongBox) setUserAuthenticationRequired( - userAutenticationRequired, - userAuthenticationTimeoutMillis, - userAuthenticationTypes + configuration.userAuthenticationRequired, + configuration.userAuthenticationTimeoutMillis, + UserAuthenticationType.decodeSet(configuration.userAuthenticationTypes) ) } diff --git a/identity-flow/build.gradle.kts b/identity-flow/build.gradle.kts index af6f87a74..f8cb47dd9 100644 --- a/identity-flow/build.gradle.kts +++ b/identity-flow/build.gradle.kts @@ -1,11 +1,49 @@ plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") alias(libs.plugins.ksp) + alias(libs.plugins.kotlinMultiplatform) } +val projectVersionCode: Int by rootProject.extra +val projectVersionName: String by rootProject.extra + kotlin { jvmToolchain(17) + + jvm() + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "identity-flow" + isStatic = true + } + } + + sourceSets { + val commonMain by getting { + kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") + dependencies { + implementation(projects.identity) + implementation(projects.processorAnnotations) + implementation(libs.kotlinx.io.bytestring) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } + + val jvmTest by getting { + kotlin.srcDir("build/generated/ksp/jvm/jvmTest/kotlin") + dependencies { + implementation(projects.processorAnnotations) + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutine.test) + } + } + } } java { @@ -13,16 +51,25 @@ java { targetCompatibility = JavaVersion.VERSION_17 } -dependencies { - implementation(project(":identity")) - implementation(project(":processor-annotations")) - ksp(project(":processor")) +group = "com.android.identity" +version = projectVersionName - implementation(libs.kotlinx.io.bytestring) - implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) +dependencies { + add("kspCommonMainMetadata", project(":processor")) + add("kspJvmTest", project(":processor")) +} - testImplementation(libs.junit) - testImplementation(libs.kotlinx.coroutine.test) +tasks.withType().all { + if (name != "kspCommonMainKotlinMetadata") { + dependsOn("kspCommonMainKotlinMetadata") + } } + +tasks["compileKotlinIosX64"].dependsOn("kspCommonMainKotlinMetadata") +tasks["compileKotlinIosArm64"].dependsOn("kspCommonMainKotlinMetadata") +tasks["compileKotlinIosSimulatorArm64"].dependsOn("kspCommonMainKotlinMetadata") + +tasks["iosX64SourcesJar"].dependsOn("kspCommonMainKotlinMetadata") +tasks["iosArm64SourcesJar"].dependsOn("kspCommonMainKotlinMetadata") +tasks["iosSimulatorArm64SourcesJar"].dependsOn("kspCommonMainKotlinMetadata") +tasks["jvmSourcesJar"].dependsOn("kspCommonMainKotlinMetadata") diff --git a/identity-flow/src/main/java/com/android/identity/flow/client/FlowBase.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/client/FlowBase.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/client/FlowBase.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/client/FlowBase.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/client/FlowNotifiable.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/client/FlowNotifiable.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/client/FlowNotifiable.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/client/FlowNotifiable.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/AesGcmCipher.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/AesGcmCipher.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/AesGcmCipher.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/AesGcmCipher.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowDispatcher.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowDispatcher.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowDispatcher.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowDispatcher.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowDispatcherHttp.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowDispatcherHttp.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowDispatcherHttp.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowDispatcherHttp.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowDispatcherLocal.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowDispatcherLocal.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowDispatcherLocal.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowDispatcherLocal.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowExceptionMap.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowExceptionMap.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowExceptionMap.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowExceptionMap.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotificationKey.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotificationKey.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotificationKey.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotificationKey.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotifications.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotifications.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotifications.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotifications.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotificationsLocal.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotificationsLocal.kt similarity index 97% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotificationsLocal.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotificationsLocal.kt index b86e58496..8f4cc51d1 100644 --- a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotificationsLocal.kt +++ b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotificationsLocal.kt @@ -21,6 +21,7 @@ import kotlin.time.Duration.Companion.milliseconds * testing/developing in local environment. */ class FlowNotificationsLocal( + private val coroutineScope: CoroutineScope, private val cipher: SimpleCipher ): FlowNotifications, FlowNotifier { private val lock = Mutex() @@ -37,7 +38,7 @@ class FlowNotificationsLocal( // as the need for the delay is an indication of a race condition somewhere // Update: it seems that we start listening for notifications a bit later than // they are emitted. - CoroutineScope(Dispatchers.IO).launch { + coroutineScope.launch { delay(200.milliseconds) val flow = lock.withLock { flowMap[ref] }; if (flow == null) { diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotificationsLocalPoll.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotificationsLocalPoll.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotificationsLocalPoll.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotificationsLocalPoll.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotifier.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotifier.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotifier.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotifier.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotifierPoll.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotifierPoll.kt similarity index 95% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotifierPoll.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotifierPoll.kt index 7f0d80486..48eabee20 100644 --- a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowNotifierPoll.kt +++ b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowNotifierPoll.kt @@ -1,14 +1,12 @@ package com.android.identity.flow.handler import com.android.identity.cbor.DataItem -import com.android.identity.flow.transport.HttpTransport import com.android.identity.util.Logger +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.transform -import kotlinx.io.bytestring.ByteString -import java.util.concurrent.CancellationException import kotlin.time.Duration.Companion.seconds /** [FlowNotifier] implementation based on [FlowPoll] interface. */ diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowPoll.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowPoll.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowPoll.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowPoll.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowPollHttp.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowPollHttp.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowPollHttp.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowPollHttp.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/FlowReturnCode.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowReturnCode.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/FlowReturnCode.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/FlowReturnCode.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/HttpHandler.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/HttpHandler.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/HttpHandler.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/HttpHandler.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/InvalidRequestException.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/InvalidRequestException.kt similarity index 70% rename from identity-flow/src/main/java/com/android/identity/flow/handler/InvalidRequestException.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/InvalidRequestException.kt index 1454e51d2..bc8ba134a 100644 --- a/identity-flow/src/main/java/com/android/identity/flow/handler/InvalidRequestException.kt +++ b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/InvalidRequestException.kt @@ -5,6 +5,6 @@ import com.android.identity.flow.annotation.FlowException @FlowException @CborSerializable -class InvalidRequestException(message: String?) : RuntimeException(message) { +class InvalidRequestException(override val message: String?) : RuntimeException(message) { companion object } diff --git a/identity-flow/src/main/java/com/android/identity/flow/handler/SimpleCipher.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/SimpleCipher.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/handler/SimpleCipher.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/handler/SimpleCipher.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/server/Configuration.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/Configuration.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/server/Configuration.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/Configuration.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/server/FlowEnvironment.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/FlowEnvironment.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/server/FlowEnvironment.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/FlowEnvironment.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/server/Resources.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/Resources.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/server/Resources.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/Resources.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/server/Storage.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/Storage.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/server/Storage.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/server/Storage.kt diff --git a/identity-flow/src/main/java/com/android/identity/flow/transport/HttpTransport.kt b/identity-flow/src/commonMain/kotlin/com/android/identity/flow/transport/HttpTransport.kt similarity index 100% rename from identity-flow/src/main/java/com/android/identity/flow/transport/HttpTransport.kt rename to identity-flow/src/commonMain/kotlin/com/android/identity/flow/transport/HttpTransport.kt diff --git a/identity-flow/src/test/java/com/android/identity/flow/FlowProcessorTest.kt b/identity-flow/src/jvmTest/kotlin/com/android/identity/flow/FlowProcessorTest.kt similarity index 93% rename from identity-flow/src/test/java/com/android/identity/flow/FlowProcessorTest.kt rename to identity-flow/src/jvmTest/kotlin/com/android/identity/flow/FlowProcessorTest.kt index 85f13403f..297fab60f 100644 --- a/identity-flow/src/test/java/com/android/identity/flow/FlowProcessorTest.kt +++ b/identity-flow/src/jvmTest/kotlin/com/android/identity/flow/FlowProcessorTest.kt @@ -19,10 +19,15 @@ import com.android.identity.flow.handler.FlowPollHttp import com.android.identity.flow.server.FlowEnvironment import com.android.identity.flow.handler.HttpHandler import kotlinx.coroutines.runBlocking -import org.junit.Assert -import org.junit.Test +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.math.sqrt import kotlin.random.Random +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.fail // The following data structures are shared between the client and the server. They can be // used as method parameters and return values. @@ -254,7 +259,7 @@ class FlowProcessorTest { runBlocking { try { localFactory().createQuadraticSolver("Foo") - Assert.fail() + fail() } catch (e: UnknownSolverException) { // expected } @@ -275,8 +280,8 @@ class FlowProcessorTest { fun nullable() { runBlocking { val factory = remoteFactory() - Assert.assertEquals("Foo", factory.nullableIdentity("Foo")) - Assert.assertNull(factory.nullableIdentity(null)) + assertEquals("Foo", factory.nullableIdentity("Foo")) + assertNull(factory.nullableIdentity(null)) } } @@ -284,11 +289,11 @@ class FlowProcessorTest { fun flowParameter() { runBlocking { val factory = localFactory() - Assert.assertEquals( + assertEquals( "MockQuadraticSolverState", factory.examineSolver(factory.createQuadraticSolver("Mock")) ) - Assert.assertEquals( + assertEquals( "DirectQuadraticSolverState[0]", factory.examineSolver(factory.createQuadraticSolver("Direct")) ) @@ -300,7 +305,7 @@ class FlowProcessorTest { runBlocking { try { remoteFactory().createQuadraticSolver("Foo") - Assert.fail() + fail() } catch (e: UnknownSolverException) { // expected } @@ -333,20 +338,20 @@ class FlowProcessorTest { fun testFlow(name: String, factory: SolverFactoryFlow) { runBlocking { val solver = factory.createQuadraticSolver(name) - Assert.assertEquals(name, solver.getName()) + assertEquals(name, solver.getName()) val solution1 = solver.solve(QuadraticEquation(a = 1.0, b = -6.0, c = 9.0)) - Assert.assertNotNull(solution1.x1) - Assert.assertEquals(3.0, solution1.x1!!, 1e-10) - Assert.assertNull(solution1.x2) + assertNotNull(solution1.x1) + assertEquals(3.0, solution1.x1, 1e-10) + assertNull(solution1.x2) try { solver.solve(QuadraticEquation(a = 1.0, b = 0.0, c = 1.0)) - Assert.fail() + fail() } catch (err: NoSolutionsException) { // expect to happen } - Assert.assertEquals(0, factory.getCount()) + assertEquals(0, factory.getCount()) solver.complete() - Assert.assertEquals(2, factory.getCount()) + assertEquals(2, factory.getCount()) } } } \ No newline at end of file diff --git a/identity-issuance-api/build.gradle.kts b/identity-issuance-api/build.gradle.kts new file mode 100644 index 000000000..eb0d3ea83 --- /dev/null +++ b/identity-issuance-api/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.ksp) + alias(libs.plugins.kotlinMultiplatform) +} + +val projectVersionCode: Int by rootProject.extra +val projectVersionName: String by rootProject.extra + +kotlin { + jvmToolchain(17) + + jvm() + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "identity-issuance-api" + isStatic = true + } + } + + sourceSets { + val commonMain by getting { + kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") + dependencies { + implementation(projects.processorAnnotations) + implementation(projects.identity) + implementation(projects.identityFlow) + implementation(libs.kotlinx.io.bytestring) + implementation(libs.kotlinx.io.core) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.coroutines.core) + } + } + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +group = "com.android.identity" +version = projectVersionName + +dependencies { + add("kspCommonMainMetadata", project(":processor")) +} + +tasks.withType().all { + if (name != "kspCommonMainKotlinMetadata") { + dependsOn("kspCommonMainKotlinMetadata") + } +} + +tasks["compileKotlinIosX64"].dependsOn("kspCommonMainKotlinMetadata") +tasks["compileKotlinIosArm64"].dependsOn("kspCommonMainKotlinMetadata") +tasks["compileKotlinIosSimulatorArm64"].dependsOn("kspCommonMainKotlinMetadata") + +tasks["iosX64SourcesJar"].dependsOn("kspCommonMainKotlinMetadata") +tasks["iosArm64SourcesJar"].dependsOn("kspCommonMainKotlinMetadata") +tasks["iosSimulatorArm64SourcesJar"].dependsOn("kspCommonMainKotlinMetadata") +tasks["jvmSourcesJar"].dependsOn("kspCommonMainKotlinMetadata") diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/ApplicationSupport.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ApplicationSupport.kt similarity index 83% rename from identity-issuance/src/main/java/com/android/identity/issuance/ApplicationSupport.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ApplicationSupport.kt index 80470a790..745a70c6c 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/ApplicationSupport.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ApplicationSupport.kt @@ -1,5 +1,7 @@ package com.android.identity.issuance +import com.android.identity.device.DeviceAssertion +import com.android.identity.device.AssertionDPoPKey import com.android.identity.flow.annotation.FlowInterface import com.android.identity.flow.annotation.FlowMethod import com.android.identity.flow.client.FlowNotifiable @@ -38,12 +40,12 @@ interface ApplicationSupport : FlowNotifiable { /** * Creates OAuth JWT client assertion based on the mobile-platform-specific [KeyAttestation] - * for the given OpenId4VCI issuance server [targetIssuanceUrl]. + * for the given OpenId4VCI issuance server specified in [AssertionDPoPKey.targetUrl]. */ @FlowMethod suspend fun createJwtClientAssertion( - clientAttestation: KeyAttestation, - targetIssuanceUrl: String + keyAttestation: KeyAttestation, + deviceAssertion: DeviceAssertion // holds AssertionDPoPKey ): String /** @@ -53,6 +55,6 @@ interface ApplicationSupport : FlowNotifiable { @FlowMethod suspend fun createJwtKeyAttestation( keyAttestations: List, - nonce: String + keysAssertion: DeviceAssertion // holds AssertionBindingKeys ): String } \ No newline at end of file diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/AuthenticationFlow.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/AuthenticationFlow.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/AuthenticationFlow.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/AuthenticationFlow.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/ClientAuthentication.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ClientAuthentication.kt similarity index 59% rename from identity-issuance/src/main/java/com/android/identity/issuance/ClientAuthentication.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ClientAuthentication.kt index dc68da8df..2f761559b 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/ClientAuthentication.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ClientAuthentication.kt @@ -1,8 +1,9 @@ package com.android.identity.issuance import com.android.identity.cbor.annotation.CborSerializable -import com.android.identity.crypto.EcSignature -import com.android.identity.crypto.X509CertChain +import com.android.identity.device.DeviceAttestation +import com.android.identity.device.AssertionNonce +import com.android.identity.device.DeviceAssertion /** * An data structure sent from the Wallet Application to the Wallet Server used to prove @@ -11,18 +12,18 @@ import com.android.identity.crypto.X509CertChain @CborSerializable data class ClientAuthentication( /** - * An ECDSA signature made by WalletApplicationKey. + * Device attestation (using clientId bytes as nonce). * - * TODO: describe what we're actually signing here. + * This is only set if this is the first time the client is authenticating. */ - val signature: EcSignature, + val attestation: DeviceAttestation?, /** - * The attestation for WalletApplicationKey. + * Assertion that proves device integrity by creating assertion for [AssertionNonce]. * - * This is only set if this is the first time the client is authenticating. + * Uses nonce from [ClientChallenge.nonce] supplied by the server. */ - val attestation: X509CertChain?, + val assertion: DeviceAssertion, /** * The capabilities of the Wallet Application. diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/ClientChallenge.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ClientChallenge.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/ClientChallenge.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ClientChallenge.kt diff --git a/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialConfiguration.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialConfiguration.kt new file mode 100644 index 000000000..4a4db73d4 --- /dev/null +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialConfiguration.kt @@ -0,0 +1,25 @@ +package com.android.identity.issuance + +import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.securearea.config.SecureAreaConfiguration +import kotlinx.io.bytestring.ByteString + +/** + * The configuration to use when creating new credentials. + */ +@CborSerializable +data class CredentialConfiguration( + /** + * The challenge to use when creating the device-bound key. + */ + val challenge: ByteString, + + /** + * The configuration for the device-bound key for e.g. access control. + * + * This is Secure Area dependent. + */ + val secureAreaConfiguration: SecureAreaConfiguration +) { + companion object +} diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/CredentialData.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialData.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/CredentialData.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialData.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/CredentialFormat.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialFormat.kt similarity index 97% rename from identity-issuance/src/main/java/com/android/identity/issuance/CredentialFormat.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialFormat.kt index 01b3ea328..f91e0142b 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/CredentialFormat.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialFormat.kt @@ -1,7 +1,5 @@ package com.android.identity.issuance -import com.android.identity.cbor.annotation.CborSerializable - /** * An enumeration of Credential Formats that an issuer may support. */ diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/CredentialRequest.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialRequest.kt similarity index 83% rename from identity-issuance/src/main/java/com/android/identity/issuance/CredentialRequest.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialRequest.kt index 16d0ddca1..b71fdc132 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/CredentialRequest.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/CredentialRequest.kt @@ -13,6 +13,4 @@ data class CredentialRequest( * was created and to be referenced in the minted credential. */ val secureAreaBoundKeyAttestation: KeyAttestation, - - // TODO: include proof that each key exist in the same device as where evidence was collected ) \ No newline at end of file diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/DocumentCondition.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/DocumentCondition.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/DocumentCondition.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/DocumentCondition.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/DocumentConfiguration.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/DocumentConfiguration.kt similarity index 88% rename from identity-issuance/src/main/java/com/android/identity/issuance/DocumentConfiguration.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/DocumentConfiguration.kt index cb67e8ba6..403970cbb 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/DocumentConfiguration.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/DocumentConfiguration.kt @@ -1,11 +1,6 @@ package com.android.identity.issuance -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborMap -import com.android.identity.cbor.DataItem -import com.android.identity.cbor.Simple import com.android.identity.cbor.annotation.CborSerializable -import com.android.identity.document.NameSpacedData /** * The configuration data for a specific document. diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/DocumentState.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/DocumentState.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/DocumentState.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/DocumentState.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthority.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthority.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthority.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthority.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthorityConfiguration.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthorityConfiguration.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthorityConfiguration.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthorityConfiguration.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthorityException.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthorityException.kt similarity index 77% rename from identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthorityException.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthorityException.kt index 6c4e25b35..7c0a96601 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthorityException.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthorityException.kt @@ -8,6 +8,6 @@ import com.android.identity.flow.annotation.FlowException */ @FlowException @CborSerializable -class IssuingAuthorityException(message: String?) : Exception(message) { +class IssuingAuthorityException(override val message: String) : Exception(message) { companion object } \ No newline at end of file diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthorityNotification.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthorityNotification.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/IssuingAuthorityNotification.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/IssuingAuthorityNotification.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/KeyPossessionChallenge.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/KeyPossessionChallenge.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/KeyPossessionChallenge.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/KeyPossessionChallenge.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/KeyPossessionProof.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/KeyPossessionProof.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/KeyPossessionProof.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/KeyPossessionProof.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/LandingUrlNotification.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/LandingUrlNotification.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/LandingUrlNotification.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/LandingUrlNotification.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/LandingUrlUnknownException.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/LandingUrlUnknownException.kt similarity index 71% rename from identity-issuance/src/main/java/com/android/identity/issuance/LandingUrlUnknownException.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/LandingUrlUnknownException.kt index a2e85c0fc..4bdcaddf8 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/LandingUrlUnknownException.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/LandingUrlUnknownException.kt @@ -5,6 +5,6 @@ import com.android.identity.flow.annotation.FlowException @FlowException @CborSerializable -class LandingUrlUnknownException(message: String?) : Exception(message) { +class LandingUrlUnknownException(override val message: String) : Exception(message) { companion object } \ No newline at end of file diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/MdocDocumentConfiguration.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/MdocDocumentConfiguration.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/MdocDocumentConfiguration.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/MdocDocumentConfiguration.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/ProofingFlow.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ProofingFlow.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/ProofingFlow.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/ProofingFlow.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/RegistrationConfiguration.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RegistrationConfiguration.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/RegistrationConfiguration.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RegistrationConfiguration.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/RegistrationFlow.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RegistrationFlow.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/RegistrationFlow.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RegistrationFlow.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/RegistrationResponse.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RegistrationResponse.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/RegistrationResponse.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RegistrationResponse.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/RequestCredentialsFlow.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RequestCredentialsFlow.kt similarity index 86% rename from identity-issuance/src/main/java/com/android/identity/issuance/RequestCredentialsFlow.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RequestCredentialsFlow.kt index 4aa8db889..caccac9e7 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/RequestCredentialsFlow.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/RequestCredentialsFlow.kt @@ -1,5 +1,6 @@ package com.android.identity.issuance +import com.android.identity.device.DeviceAssertion import com.android.identity.flow.client.FlowBase import com.android.identity.flow.annotation.FlowInterface import com.android.identity.flow.annotation.FlowMethod @@ -35,7 +36,10 @@ interface RequestCredentialsFlow : FlowBase { * @throws IllegalArgumentException if the issuer rejects the one or more of the requests. */ @FlowMethod - suspend fun sendCredentials(credentialRequests: List): List + suspend fun sendCredentials( + credentialRequests: List, + keysAssertion: DeviceAssertion // wraps AssertionBindingKeys + ): List @FlowMethod suspend fun sendPossessionProofs(keyPossessionProofs: List) diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/SdJwtVcDocumentConfiguration.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/SdJwtVcDocumentConfiguration.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/SdJwtVcDocumentConfiguration.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/SdJwtVcDocumentConfiguration.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/WalletApplicationCapabilities.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletApplicationCapabilities.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/WalletApplicationCapabilities.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletApplicationCapabilities.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/WalletServer.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletServer.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/WalletServer.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletServer.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/WalletServerCapabilities.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletServerCapabilities.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/WalletServerCapabilities.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletServerCapabilities.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/WalletServerSettings.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletServerSettings.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/WalletServerSettings.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/WalletServerSettings.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequest.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequest.kt similarity index 85% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequest.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequest.kt index 250ac0df6..15b214b6b 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequest.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequest.kt @@ -1,7 +1,6 @@ package com.android.identity.issuance.evidence import com.android.identity.cbor.annotation.CborSerializable -import com.android.identity.cbor.Cbor /** * A request for evidence by the issuer. diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestCompletionMessage.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestCompletionMessage.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestCompletionMessage.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestCompletionMessage.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestCreatePassphrase.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestCreatePassphrase.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestCreatePassphrase.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestCreatePassphrase.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestGermanEid.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestGermanEid.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestGermanEid.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestGermanEid.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnel.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnel.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnel.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnel.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnelType.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnelType.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnelType.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestIcaoNfcTunnelType.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestIcaoPassiveAuthentication.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestIcaoPassiveAuthentication.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestIcaoPassiveAuthentication.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestIcaoPassiveAuthentication.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestMessage.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestNotificationPermission.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestNotificationPermission.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestNotificationPermission.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestNotificationPermission.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestOpenid4Vp.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestOpenid4Vp.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestOpenid4Vp.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestOpenid4Vp.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestPreauthorizedCode.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestPreauthorizedCode.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestPreauthorizedCode.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestPreauthorizedCode.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestQuestionMultipleChoice.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestQuestionMultipleChoice.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestQuestionMultipleChoice.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestQuestionMultipleChoice.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestQuestionString.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestQuestionString.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestQuestionString.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestQuestionString.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestSelfieVideo.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestSelfieVideo.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestSelfieVideo.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestSelfieVideo.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestSetupCloudSecureArea.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestSetupCloudSecureArea.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestSetupCloudSecureArea.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestSetupCloudSecureArea.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestWeb.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestWeb.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceRequestWeb.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceRequestWeb.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponse.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponse.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponse.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponse.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseCreatePassphrase.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseCreatePassphrase.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseCreatePassphrase.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseCreatePassphrase.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseGermanEid.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseGermanEid.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseGermanEid.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseGermanEid.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseGermanEidResolved.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseGermanEidResolved.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseGermanEidResolved.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseGermanEidResolved.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnel.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnel.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnel.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnel.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnelResult.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnelResult.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnelResult.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseIcaoNfcTunnelResult.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseIcaoPassiveAuthentication.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseIcaoPassiveAuthentication.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseIcaoPassiveAuthentication.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseIcaoPassiveAuthentication.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseMessage.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseMessage.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseMessage.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseMessage.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseNotificationPermission.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseNotificationPermission.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseNotificationPermission.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseNotificationPermission.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseOpenid4Vp.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseOpenid4Vp.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseOpenid4Vp.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseOpenid4Vp.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponsePreauthorizedCode.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponsePreauthorizedCode.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponsePreauthorizedCode.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponsePreauthorizedCode.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseQuestionMultipleChoice.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseQuestionMultipleChoice.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseQuestionMultipleChoice.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseQuestionMultipleChoice.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseQuestionString.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseQuestionString.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseQuestionString.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseQuestionString.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseSelfieVideo.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseSelfieVideo.kt similarity index 79% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseSelfieVideo.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseSelfieVideo.kt index 28e4af747..7a38443a1 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseSelfieVideo.kt +++ b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseSelfieVideo.kt @@ -4,9 +4,7 @@ data class EvidenceResponseSelfieVideo(val selfieImage: ByteArray) : EvidenceResponse() { override fun equals(other: Any?): Boolean { if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as EvidenceResponseSelfieVideo + if (other !is EvidenceResponseSelfieVideo) return false return selfieImage.contentEquals(other.selfieImage) } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseSetupCloudSecureArea.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseSetupCloudSecureArea.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseSetupCloudSecureArea.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseSetupCloudSecureArea.kt diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseWeb.kt b/identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseWeb.kt similarity index 100% rename from identity-issuance/src/main/java/com/android/identity/issuance/evidence/EvidenceResponseWeb.kt rename to identity-issuance-api/src/commonMain/kotlin/com/android/identity/issuance/evidence/EvidenceResponseWeb.kt diff --git a/identity-issuance/build.gradle.kts b/identity-issuance/build.gradle.kts index b72e35865..acefac005 100644 --- a/identity-issuance/build.gradle.kts +++ b/identity-issuance/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":identity-mdoc")) implementation(project(":identity-sdjwt")) implementation(project(":identity-flow")) + implementation(project(":identity-issuance-api")) implementation(project(":mrtd-reader")) implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.json) diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/CredentialConfiguration.kt b/identity-issuance/src/main/java/com/android/identity/issuance/CredentialConfiguration.kt deleted file mode 100644 index b847c5042..000000000 --- a/identity-issuance/src/main/java/com/android/identity/issuance/CredentialConfiguration.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.android.identity.issuance - -import com.android.identity.cbor.annotation.CborSerializable - -/** - * The configuration to use when creating new credentials. - */ -@CborSerializable -data class CredentialConfiguration( - /** - * The challenge to use when creating the device-bound key. - */ - val challenge: ByteArray, - - /** - * The Secure Area to use for the device-bound key, - */ - val secureAreaIdentifier: String, - - /** - * The configuration for the device-bound key for e.g. access control. - * - * This is Secure Area dependent and CBOR encoded as a map with keys encoded as textual strings. - * - * For [SoftwareSecureArea] the following keys are recognized: - * - `purposes: the value is a number encoded like in [Keypurpose.Companion.encodeSet]. - * - `curve`: the value is a number encoded like [EcCurve.coseCurveIdentifier]. - * - `passphrase`: the value is a tstr with the passphrase to use. - * - `passphraseConstraints`: the value is a CBOR-serialized [PassphraseConstraints] object. - * - * For [AndroidKeystoreSecureArea] the following keys are recognized: - * - `purposes`: the value is a number encoded like in [Keypurpose.Companion.encodeSet]. - * - `curve`: the value is a number encoded like [EcCurve.coseCurveIdentifier]. - * - `useStrongbox`: a boolean, true to use StrongBox, false otherwise. - * - `userAuthenticationRequired`: a boolean specifying whether to require user authentication. - * - `userAuthenticationTimeoutMillis`: a number with the user authentication timeout in milliseconds - * or 0 to require authentication on every use. - * - `userAuthenticationTypes`: the value is a number like in [UserAuthenticationType.Companion.encodeSet]. - * - * For [CloudSecureArea] the following keys are recognized - * - `purposes`: the value is a number encoded like in [Keypurpose.Companion.encodeSet]. - * - `curve`: the value is a number encoded like [EcCurve.coseCurveIdentifier]. - * - `userAuthenticationRequired`: a boolean specifying whether to require user authentication. - * - `userAuthenticationTimeoutMillis`: a number with the user authentication timeout in milliseconds - * or 0 to require authentication on every use. - * - `userAuthenticationTypes`: the value is a number like in [UserAuthenticationType.Companion.encodeSet]. - * - `passphraseRequired`: a boolean specifying whether to require passphrase authentication. - */ - val secureAreaConfiguration: ByteArray -) diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt b/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt index 389ad7d83..3c4ceb808 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/authenticationUtilities.kt @@ -1,42 +1,36 @@ package com.android.identity.issuance +import com.android.identity.crypto.Algorithm +import com.android.identity.crypto.Crypto +import com.android.identity.crypto.EcSignature +import com.android.identity.crypto.X509Cert import com.android.identity.crypto.X509CertChain import com.android.identity.crypto.javaX509Certificate import com.android.identity.crypto.javaX509Certificates +import com.android.identity.device.AssertionBindingKeys +import com.android.identity.device.DeviceAssertion +import com.android.identity.device.DeviceAttestation +import com.android.identity.device.DeviceAttestationAndroid +import com.android.identity.device.DeviceAttestationIos +import com.android.identity.device.DeviceAttestationJvm +import com.android.identity.flow.server.Configuration +import com.android.identity.flow.server.FlowEnvironment +import com.android.identity.issuance.common.cache import com.android.identity.securearea.AttestationExtension +import com.android.identity.securearea.KeyAttestation import com.android.identity.util.AndroidAttestationExtensionParser import com.android.identity.util.Logger import com.android.identity.util.fromHex import com.android.identity.util.toHex import kotlinx.io.bytestring.ByteString -import kotlinx.io.bytestring.ByteStringBuilder -import kotlinx.io.bytestring.append import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.ASN1OctetString -import org.bouncycastle.asn1.ASN1Sequence -private val salt = byteArrayOf((0xe7).toByte(), 0x7c, (0xf8).toByte(), (0xec).toByte()) - -private const val KEY_DESCRIPTION_OID: String = "1.3.6.1.4.1.11129.2.1.17" +// TODO: move as much of this as possible into com.android.identity.device (and perhaps +// com.android.identity.crypto) package. private const val TAG = "authenticationUtilities" -fun authenticationMessage(clientId: String, nonce: ByteString): ByteString { - val buffer = ByteStringBuilder() - buffer.append(salt) - buffer.append(clientId.toByteArray()) - buffer.append(nonce) - return buffer.toByteString() -} - -fun extractAttestationSequence(chain: X509CertChain): ASN1Sequence { - val extension = chain.certificates[0].javaX509Certificate.getExtensionValue(KEY_DESCRIPTION_OID) - val asn1InputStream = ASN1InputStream(extension) - val derSequenceBytes = (asn1InputStream.readObject() as ASN1OctetString).octets - val seqInputStream = ASN1InputStream(derSequenceBytes) - return seqInputStream.readObject() as ASN1Sequence -} - private fun X509CertChain.validate() { val certs = certificates // Check that all the certificates sign each other... @@ -51,9 +45,9 @@ private fun X509CertChain.validate() { } } -fun validateKeyAttestation( +fun validateAndroidKeyAttestation( chain: X509CertChain, - clientId: String?, + nonce: ByteString?, requireGmsAttestation: Boolean, requireVerifiedBootGreen: Boolean, requireAppSignatureCertificateDigests: List, @@ -74,8 +68,9 @@ fun validateKeyAttestation( val parser = AndroidAttestationExtensionParser(x509certs[0]) // Challenge must match... - check(clientId == null || clientId.toByteArray() contentEquals parser.attestationChallenge) - { "Challenge didn't match what was expected" } + check(nonce == null || nonce == ByteString(parser.attestationChallenge)) { + "Challenge didn't match what was expected" + } if (requireVerifiedBootGreen) { // Verified Boot state must VERIFIED @@ -85,7 +80,7 @@ fun validateKeyAttestation( ) { "Verified boot state is not GREEN" } } - if (requireAppSignatureCertificateDigests.size > 0) { + if (requireAppSignatureCertificateDigests.isNotEmpty()) { check (parser.applicationSignatureDigests.size == requireAppSignatureCertificateDigests.size) { "Number Signing certificates mismatch" } for (n in 0.. ) { chain.validate() @@ -135,13 +131,141 @@ fun validateCloudKeyAttestation( throw IllegalStateException("Error decoding attestation extension", e) } - val challengeInAttestation = AttestationExtension.decode(attestationExtension) - if (!challengeInAttestation.contentEquals(nonce.toByteArray())) { - throw IllegalStateException("Challenge in attestation does match expected attestation") + val challengeInAttestation = ByteString(AttestationExtension.decode(attestationExtension)) + if (challengeInAttestation != nonce) { + throw IllegalStateException("Challenge in attestation does match expected nonce") } val rootPublicKey = ByteString(certificates.last().javaX509Certificate.publicKey.encoded) check(trustedRootKeys.contains(rootPublicKey)) { "Unexpected cloud attestation root" } -} \ No newline at end of file +} + +fun validateIosDeviceAttestation(attestation: DeviceAttestationIos) { + // TODO, assume valid for now +} + +fun validateDeviceAttestation( + attestation: DeviceAttestation, + clientId: String, + settings: WalletServerSettings +) { + when (attestation) { + is DeviceAttestationAndroid -> { + validateAndroidKeyAttestation( + attestation.certificateChain, + null, // TODO: enable: ByteString(clientId.toByteArray()), + settings.androidRequireGmsAttestation, + settings.androidRequireVerifiedBootGreen, + settings.androidRequireAppSignatureCertificateDigests + ) + } + is DeviceAttestationIos -> { + validateIosDeviceAttestation(attestation) + } + is DeviceAttestationJvm -> + throw IllegalArgumentException("JVM attestations are not accepted") + } +} + +fun validateDeviceAssertion(attestation: DeviceAttestation, assertion: DeviceAssertion) { + try { + when (attestation) { + is DeviceAttestationAndroid -> { + val signature = + EcSignature.fromCoseEncoded(assertion.platformAssertion.toByteArray()) + if (!Crypto.checkSignature( + publicKey = attestation.certificateChain.certificates.first().ecPublicKey, + message = assertion.assertionData.toByteArray(), + algorithm = Algorithm.ES256, + signature = signature + ) + ) { + throw IllegalArgumentException("DeviceAssertion validation failed") + } + } + + is DeviceAttestationIos -> { + // accept for now + } + + is DeviceAttestationJvm -> + throw IllegalArgumentException("JVM attestations are not accepted") + } + } catch(err: Exception) { + err.printStackTrace() + throw err + } +} + +suspend fun validateDeviceAssertionBindingKeys( + env: FlowEnvironment, + deviceAttestation: DeviceAttestation, + keyAttestations: List, + deviceAssertion: DeviceAssertion, + nonce: ByteString? +): AssertionBindingKeys { + val settings = WalletServerSettings(env.getInterface(Configuration::class)!!) + if (env.getInterface(ApplicationSupport::class) == null) { + // No ApplicationSupport is indication that we are running on the server, not + // locally in app. Device assertion validation is only meaningful or possible + // on the server. + validateDeviceAssertion(deviceAttestation, deviceAssertion) + } + val assertion = deviceAssertion.assertion as AssertionBindingKeys + check(nonce == null || nonce == assertion.nonce) + + val keyList = keyAttestations.map { attestation -> + val certChain = attestation.certChain + if (certChain == null) { + if (deviceAttestation !is DeviceAttestationIos) { + throw IllegalArgumentException("key attestations are only optional for iOS") + } + } else { + // TODO: check that what is claimed in the assertion matches what we see in key + // attestations + check(attestation.publicKey == certChain.certificates.first().ecPublicKey) + if (isCloudKeyAttestation(certChain)) { + val trustedRootKeys = getCloudSecureAreaTrustedRootKeys(env) + validateCloudKeyAttestation( + attestation.certChain!!, + assertion.nonce, + trustedRootKeys.trustedKeys + ) + } else { + validateAndroidKeyAttestation( + certChain, + assertion.nonce, + settings.androidRequireGmsAttestation, + settings.androidRequireVerifiedBootGreen, + settings.androidRequireAppSignatureCertificateDigests + ) + } + } + attestation.publicKey + } + + if (keyList != assertion.publicKeys) { + throw IllegalArgumentException("key list mismatch") + } + + return assertion +} + +private suspend fun getCloudSecureAreaTrustedRootKeys( + env: FlowEnvironment +): CloudSecureAreaTrustedRootKeys { + return env.cache(CloudSecureAreaTrustedRootKeys::class) { configuration, resources -> + val certificateName = configuration.getValue("csa.certificate") + ?: "cloud_secure_area/certificate.pem" + val certificate = X509Cert.fromPem(resources.getStringResource(certificateName)!!) + CloudSecureAreaTrustedRootKeys( + trustedKeys = setOf(ByteString(certificate.javaX509Certificate.publicKey.encoded)) + ) + } +} + +internal data class CloudSecureAreaTrustedRootKeys( + val trustedKeys: Set +) diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/AbstractRequestCredentials.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/AbstractRequestCredentials.kt index 63d9f3ec2..7502ac718 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/AbstractRequestCredentials.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/AbstractRequestCredentials.kt @@ -1,6 +1,5 @@ package com.android.identity.issuance.funke -import com.android.identity.cbor.annotation.CborSerializable import com.android.identity.flow.annotation.FlowState import com.android.identity.issuance.CredentialConfiguration import com.android.identity.issuance.CredentialFormat @@ -12,7 +11,6 @@ import com.android.identity.issuance.RequestCredentialsFlow abstract class AbstractRequestCredentials( val documentId: String, val credentialConfiguration: CredentialConfiguration, - val nonce: String, var format: CredentialFormat? = null, ) { companion object diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/CredentialRequestSet.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/CredentialRequestSet.kt new file mode 100644 index 000000000..9ee2477a3 --- /dev/null +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/CredentialRequestSet.kt @@ -0,0 +1,15 @@ +package com.android.identity.issuance.funke + +import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.device.DeviceAssertion +import com.android.identity.issuance.CredentialFormat +import com.android.identity.securearea.KeyAttestation + +@CborSerializable +class CredentialRequestSet( + val format: CredentialFormat, + val keyAttestations: List, + val keysAssertion: DeviceAssertion // holds AssertionBindingKeys +) { + companion object +} diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt index 539ba4d82..827ba38c4 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeIssuingAuthorityState.kt @@ -1,12 +1,13 @@ package com.android.identity.issuance.funke import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborMap import com.android.identity.cbor.annotation.CborSerializable import com.android.identity.crypto.Algorithm import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcCurve import com.android.identity.crypto.EcPublicKey +import com.android.identity.device.AssertionDPoPKey +import com.android.identity.device.DeviceAssertionMaker import com.android.identity.document.NameSpacedData import com.android.identity.documenttype.DocumentType import com.android.identity.documenttype.DocumentTypeRepository @@ -36,6 +37,8 @@ import com.android.identity.issuance.IssuingAuthorityNotification import com.android.identity.issuance.MdocDocumentConfiguration import com.android.identity.issuance.RegistrationResponse import com.android.identity.issuance.SdJwtVcDocumentConfiguration +import com.android.identity.securearea.config.SecureAreaConfigurationAndroidKeystore +import com.android.identity.securearea.config.SecureAreaConfigurationCloud import com.android.identity.issuance.WalletApplicationCapabilities import com.android.identity.issuance.common.AbstractIssuingAuthorityState import com.android.identity.issuance.common.cache @@ -340,46 +343,43 @@ class FunkeIssuingAuthorityState( refreshAccessIfNeeded(env, documentId, document) val cNonce = document.access!!.cNonce!! + val purposes = setOf(KeyPurpose.SIGN, KeyPurpose.AGREE_KEY) val configuration = if (document.secureAreaIdentifier!!.startsWith("CloudSecureArea?")) { - val purposes = setOf(KeyPurpose.SIGN, KeyPurpose.AGREE_KEY) CredentialConfiguration( - cNonce.toByteArray(), - document.secureAreaIdentifier!!, - Cbor.encode( - CborMap.builder() - .put("passphraseRequired", true) - .put("userAuthenticationRequired", true) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", 3 /* LSKF + Biometrics */) - .put("purposes", KeyPurpose.encodeSet(purposes)) - .end().build() + ByteString(cNonce.toByteArray()), + SecureAreaConfigurationCloud( + purposes = KeyPurpose.encodeSet(purposes), + curve = EcCurve.P256.coseCurveIdentifier, + cloudSecureAreaId = document.secureAreaIdentifier!!, + passphraseRequired = true, + useStrongBox = true, + userAuthenticationRequired = true, + userAuthenticationTimeoutMillis = 0, + userAuthenticationTypes = 3, // LSKF + Biometrics ) ) } else { CredentialConfiguration( - cNonce.toByteArray(), - document.secureAreaIdentifier!!, - Cbor.encode( - CborMap.builder() - .put("curve", EcCurve.P256.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN))) - .put("useStrongBox", true) - .put("userAuthenticationRequired", true) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", 3 /* LSKF + Biometrics */) - .end().build() + ByteString(cNonce.toByteArray()), + SecureAreaConfigurationAndroidKeystore( + purposes = KeyPurpose.encodeSet(purposes), + curve = EcCurve.P256.coseCurveIdentifier, + useStrongBox = true, + userAuthenticationRequired = true, + userAuthenticationTimeoutMillis = 0, + userAuthenticationTypes = 3 // LSKF + Biometrics ) ) } return when (credentialConfiguration.proofType) { is Openid4VciProofTypeKeyAttestation -> - RequestCredentialsUsingKeyAttestation(documentId, configuration, cNonce) + RequestCredentialsUsingKeyAttestation(clientId, documentId, configuration) is Openid4VciProofTypeJwt -> RequestCredentialsUsingProofOfPossession( - issuanceClientId, - documentId, - configuration, - cNonce, + clientId = clientId, + issuanceClientId = issuanceClientId, + documentId = documentId, + credentialConfiguration = configuration, credentialIssuerUri = credentialIssuerUri ) Openid4VciNoProof -> @@ -394,9 +394,9 @@ class FunkeIssuingAuthorityState( val metadata = Openid4VciIssuerMetadata.get(env, credentialIssuerUri) val credentialConfiguration = metadata.credentialConfigurations[credentialConfigurationId]!! - val (request, publicKeys) = when (state) { + val credentialTasks = when (state) { is RequestCredentialsUsingProofOfPossession -> - createRequestUsingProofOfPossession(state, credentialConfiguration) + listOf(createRequestUsingProofOfPossession(state, credentialConfiguration)) is RequestCredentialsUsingKeyAttestation -> createRequestUsingKeyAttestation(env, state, credentialConfiguration) else -> throw IllegalStateException("Unsupported RequestCredential type") @@ -404,43 +404,48 @@ class FunkeIssuingAuthorityState( val document = loadIssuerDocument(env, state.documentId) - // Send the request - val credentials = obtainCredentials(env, metadata, request, state.documentId, document) - - check(credentials.size == publicKeys.size) - document.credentials.addAll(credentials.zip(publicKeys).map { - val credential = it.first.jsonPrimitive.content - val publicKey = it.second - when (credentialConfiguration.format) { - is Openid4VciFormatSdJwt -> { - val sdJwt = SdJwtVerifiableCredential.fromString(credential) - val jwtBody = JwtBody.fromString(sdJwt.body) - CredentialData( - publicKey, - jwtBody.timeValidityBegin ?: jwtBody.timeSigned ?: Clock.System.now(), - jwtBody.timeValidityEnd ?: Instant.DISTANT_FUTURE, - CredentialFormat.SD_JWT_VC, - credential.toByteArray() - ) - } - is Openid4VciFormatMdoc -> { - val credentialBytes = credential.fromBase64Url() - val credentialData = StaticAuthDataParser(credentialBytes).parse() - val issuerAuthCoseSign1 = Cbor.decode(credentialData.issuerAuth).asCoseSign1 - val encodedMsoBytes = Cbor.decode(issuerAuthCoseSign1.payload!!) - val encodedMso = Cbor.encode(encodedMsoBytes.asTaggedEncodedCbor) - val mso = MobileSecurityObjectParser(encodedMso).parse() - CredentialData( - publicKey, - mso.validFrom, - mso.validUntil, - CredentialFormat.MDOC_MSO, - credentialBytes - ) + for ((request, publicKeys) in credentialTasks) { + // Send the request + val credentials = obtainCredentials(env, metadata, request, state.documentId, document) + + check(credentials.size == publicKeys.size) + document.credentials.addAll(credentials.zip(publicKeys).map { + val credential = it.first.jsonPrimitive.content + val publicKey = it.second + when (credentialConfiguration.format) { + is Openid4VciFormatSdJwt -> { + val sdJwt = SdJwtVerifiableCredential.fromString(credential) + val jwtBody = JwtBody.fromString(sdJwt.body) + CredentialData( + publicKey, + jwtBody.timeValidityBegin ?: jwtBody.timeSigned ?: Clock.System.now(), + jwtBody.timeValidityEnd ?: Instant.DISTANT_FUTURE, + CredentialFormat.SD_JWT_VC, + credential.toByteArray() + ) + } + + is Openid4VciFormatMdoc -> { + val credentialBytes = credential.fromBase64Url() + val credentialData = StaticAuthDataParser(credentialBytes).parse() + val issuerAuthCoseSign1 = Cbor.decode(credentialData.issuerAuth).asCoseSign1 + val encodedMsoBytes = Cbor.decode(issuerAuthCoseSign1.payload!!) + val encodedMso = Cbor.encode(encodedMsoBytes.asTaggedEncodedCbor) + val mso = MobileSecurityObjectParser(encodedMso).parse() + CredentialData( + publicKey, + mso.validFrom, + mso.validUntil, + CredentialFormat.MDOC_MSO, + credentialBytes + ) + } + + null -> throw IllegalStateException("Unexpected credential format") } - null -> throw IllegalStateException("Unexpected credential format") - } - }) + }) + } + updateIssuerDocument(env, state.documentId, document, true) } @@ -562,29 +567,32 @@ class FunkeIssuingAuthorityState( env: FlowEnvironment, state: RequestCredentialsUsingKeyAttestation, configuration: Openid4VciCredentialConfiguration - ): Pair> { + ): List>> { // NB: applicationSupport will only be non-null in the environment when running this code // locally in the Android Wallet app. val applicationSupport = env.getInterface(ApplicationSupport::class) - - val platformAttestations = - state.credentialRequests!!.map { it.secureAreaBoundKeyAttestation } - - val keyAttestation = - applicationSupport?.createJwtKeyAttestation(platformAttestations, state.nonce) - ?: ApplicationSupportState(clientId).createJwtKeyAttestation( - env, platformAttestations, state.nonce + return state.credentialRequestSets.map { credentialRequestSet -> + val jwtKeyAttestation = if (applicationSupport != null) { + applicationSupport.createJwtKeyAttestation( + keyAttestations = credentialRequestSet.keyAttestations, + keysAssertion = credentialRequestSet.keysAssertion ) - - val request = buildJsonObject { - put("proof", buildJsonObject { - put("attestation", JsonPrimitive(keyAttestation)) - put("proof_type", JsonPrimitive("attestation")) - }) - putFormat(configuration.format!!) + } else { + ApplicationSupportState(clientId).createJwtKeyAttestation( + env = env, + keyAttestations = credentialRequestSet.keyAttestations, + keysAssertion = credentialRequestSet.keysAssertion + ) + } + val request = buildJsonObject { + put("proof", buildJsonObject { + put("attestation", JsonPrimitive(jwtKeyAttestation)) + put("proof_type", JsonPrimitive("attestation")) + }) + putFormat(configuration.format!!) + } + Pair(request, credentialRequestSet.keyAttestations.map { it.publicKey }) } - - return Pair(request, platformAttestations.map { it.publicKey }) } @FlowMethod @@ -691,14 +699,23 @@ class FunkeIssuingAuthorityState( } val clientKeyInfo = FunkeUtil.communicationKey(env, clientId) - val clientAssertion = applicationSupport?.createJwtClientAssertion( - clientKeyInfo.attestation, - credentialIssuerUri - ) ?: ApplicationSupportState(clientId).createJwtClientAssertion( - env, - clientKeyInfo.publicKey, - credentialIssuerUri - ) + val clientAssertion = if (applicationSupport != null) { + // Required when applicationSupport is exposed + val assertionMaker = env.getInterface(DeviceAssertionMaker::class)!! + applicationSupport.createJwtClientAssertion( + clientKeyInfo.attestation, + assertionMaker.makeDeviceAssertion(AssertionDPoPKey( + clientKeyInfo.publicKey, + credentialIssuerUri + )) + ) + } else { + ApplicationSupportState(clientId).createJwtClientAssertion( + env, + clientKeyInfo.publicKey, + credentialIssuerUri + ) + } val req = FormUrlEncoder { add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-client-attestation") diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt index 4d0c375b2..769f8d906 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/FunkeProofingState.kt @@ -95,7 +95,8 @@ class FunkeProofingState( ) ) } else if (!notificationPermissonRequested) { - listOf(EvidenceRequestNotificationPermission( + listOf( + EvidenceRequestNotificationPermission( permissionNotGrantedMessage = """ ## Receive notifications? @@ -110,7 +111,8 @@ class FunkeProofingState( grantPermissionButtonText = "Grant Permission", continueWithoutPermissionButtonText = "No Thanks", assets = mapOf() - )) + ) + ) } else { val list = mutableListOf(EvidenceRequestPreauthorizedCode()) if (proofingInfo != null && metadata.authorizationServers.isNotEmpty()) { @@ -175,8 +177,11 @@ class FunkeProofingState( @FlowMethod suspend fun sendEvidence(env: FlowEnvironment, evidenceResponse: EvidenceResponse) { when (evidenceResponse) { - is EvidenceResponseGermanEid -> if (evidenceResponse.url != null) { - processRedirectUrl(env, evidenceResponse.url) + is EvidenceResponseGermanEid -> { + val url = evidenceResponse.url + if (url != null) { + processRedirectUrl(env, url) + } } is EvidenceResponseWeb -> { val index = evidenceResponse.response.indexOf("code=") diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt index dd4b502a5..6efa1f476 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingKeyAttestation.kt @@ -1,6 +1,7 @@ package com.android.identity.issuance.funke import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.device.DeviceAssertion import com.android.identity.flow.annotation.FlowMethod import com.android.identity.flow.annotation.FlowState import com.android.identity.flow.server.FlowEnvironment @@ -16,12 +17,12 @@ import com.android.identity.issuance.RequestCredentialsFlow ) @CborSerializable class RequestCredentialsUsingKeyAttestation( + val clientId: String, documentId: String, credentialConfiguration: CredentialConfiguration, - nonce: String, format: CredentialFormat? = null, - var credentialRequests: List? = null -) : AbstractRequestCredentials(documentId, credentialConfiguration, nonce, format) { + val credentialRequestSets: MutableList = mutableListOf() +) : AbstractRequestCredentials(documentId, credentialConfiguration, format) { companion object @FlowMethod @@ -36,13 +37,15 @@ class RequestCredentialsUsingKeyAttestation( @FlowMethod fun sendCredentials( env: FlowEnvironment, - newCredentialRequests: List + credentialRequests: List, + keysAssertion: DeviceAssertion // holds AssertionBingingKeys ): List { - if (credentialRequests != null) { - throw IllegalStateException("Credential requests were already sent") - } - credentialRequests = newCredentialRequests - return listOf() + credentialRequestSets.add(CredentialRequestSet( + format = format!!, + keyAttestations = credentialRequests.map { it.secureAreaBoundKeyAttestation }, + keysAssertion = keysAssertion + )) + return emptyList() } @FlowMethod diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt index b86a6f963..203a1d03f 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/funke/RequestCredentialsUsingProofOfPossession.kt @@ -1,15 +1,20 @@ package com.android.identity.issuance.funke import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.device.DeviceAssertion import com.android.identity.flow.annotation.FlowMethod import com.android.identity.flow.annotation.FlowState import com.android.identity.flow.server.FlowEnvironment +import com.android.identity.flow.server.Storage import com.android.identity.issuance.CredentialConfiguration import com.android.identity.issuance.CredentialFormat import com.android.identity.issuance.CredentialRequest import com.android.identity.issuance.KeyPossessionChallenge import com.android.identity.issuance.KeyPossessionProof import com.android.identity.issuance.RequestCredentialsFlow +import com.android.identity.issuance.validateDeviceAssertionBindingKeys +import com.android.identity.issuance.wallet.ClientRecord +import com.android.identity.issuance.wallet.fromCbor import com.android.identity.util.toBase64Url import kotlinx.datetime.Clock import kotlinx.io.bytestring.ByteString @@ -21,14 +26,14 @@ import kotlinx.serialization.json.JsonPrimitive ) @CborSerializable class RequestCredentialsUsingProofOfPossession( + val clientId: String, val issuanceClientId: String, documentId: String, credentialConfiguration: CredentialConfiguration, - nonce: String, val credentialIssuerUri: String, format: CredentialFormat? = null, var credentialRequests: List? = null, -) : AbstractRequestCredentials(documentId, credentialConfiguration, nonce, format) { +) : AbstractRequestCredentials(documentId, credentialConfiguration, format) { companion object @FlowMethod @@ -41,13 +46,25 @@ class RequestCredentialsUsingProofOfPossession( } @FlowMethod - fun sendCredentials( + suspend fun sendCredentials( env: FlowEnvironment, - newCredentialRequests: List + newCredentialRequests: List, + keysAssertion: DeviceAssertion ): List { if (credentialRequests != null) { throw IllegalStateException("Credentials were already sent") } + val storage = env.getInterface(Storage::class)!! + val clientRecord = ClientRecord.fromCbor( + storage.get("Clients", "", clientId)!!.toByteArray()) + validateDeviceAssertionBindingKeys( + env = env, + deviceAttestation = clientRecord.deviceAttestation, + keyAttestations = newCredentialRequests.map { it.secureAreaBoundKeyAttestation }, + deviceAssertion = keysAssertion, + nonce = credentialConfiguration.challenge + ) + val nonce = JsonPrimitive(String(credentialConfiguration.challenge.toByteArray())) val requests = newCredentialRequests.map { request -> val header = JsonObject(mapOf( "typ" to JsonPrimitive("openid4vci-proof+jwt"), @@ -58,7 +75,7 @@ class RequestCredentialsUsingProofOfPossession( "iss" to JsonPrimitive(issuanceClientId), "aud" to JsonPrimitive(credentialIssuerUri), "iat" to JsonPrimitive(Clock.System.now().epochSeconds), - "nonce" to JsonPrimitive(nonce) + "nonce" to nonce )).toString().toByteArray().toBase64Url() ProofOfPossessionCredentialRequest(request, format!!, "$header.$body") } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuerDocument.kt b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuerDocument.kt index 20354ca25..09568ef23 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuerDocument.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuerDocument.kt @@ -11,7 +11,6 @@ import com.android.identity.issuance.RegistrationResponse import com.android.identity.issuance.evidence.EvidenceResponse import com.android.identity.issuance.fromDataItem import com.android.identity.issuance.toDataItem -import kotlinx.datetime.Instant // The document as seen from the issuer's perspective data class IssuerDocument( diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt index b0ba4c7c8..e8e9afe87 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/IssuingAuthorityState.kt @@ -289,35 +289,31 @@ class IssuingAuthorityState( walletApplicationCapabilities, issuerDocument.collectedEvidence ) - return RequestCredentialsState( - documentId, - credentialConfiguration) + return RequestCredentialsState(clientId, documentId, credentialConfiguration) } @FlowJoin suspend fun completeRequestCredentials(env: FlowEnvironment, state: RequestCredentialsState) { - val now = Clock.System.now() - val issuerDocument = loadIssuerDocument(env, state.documentId) - for (request in state.credentialRequests) { + for (bindingKeySet in state.bindingKeys) { // Skip if we already have a request for the authentication key - if (hasCpoRequestForAuthenticationKey(issuerDocument, - request.secureAreaBoundKeyAttestation.publicKey)) { - continue + for (authenticationKey in bindingKeySet.publicKeys) { + if (hasCpoRequestForAuthenticationKey(issuerDocument, authenticationKey)) { + continue + } + val presentationData = createPresentationData( + env, + state.format!!, + issuerDocument.documentConfiguration!!, + authenticationKey + ) + val simpleCredentialRequest = SimpleCredentialRequest( + authenticationKey, + CredentialFormat.MDOC_MSO, + presentationData, + ) + issuerDocument.simpleCredentialRequests.add(simpleCredentialRequest) } - val authenticationKey = request.secureAreaBoundKeyAttestation.publicKey - val presentationData = createPresentationData( - env, - state.format!!, - issuerDocument.documentConfiguration!!, - authenticationKey - ) - val simpleCredentialRequest = SimpleCredentialRequest( - authenticationKey, - CredentialFormat.MDOC_MSO, - presentationData, - ) - issuerDocument.simpleCredentialRequests.add(simpleCredentialRequest) } updateIssuerDocument(env, state.documentId, issuerDocument) } @@ -1078,11 +1074,12 @@ class IssuingAuthorityState( env: FlowEnvironment, germanEid: EvidenceResponseGermanEidResolved ): JsonObject { - if (germanEid.data == null) { + val germanEidData = germanEid.data + if (germanEidData == null) { Logger.e(TAG, "No data in eId response") throw IllegalStateException("No personal data") } - val elem = Json.parseToJsonElement(germanEid.data) + val elem = Json.parseToJsonElement(germanEidData) val personalData = elem.jsonObject["PersonalData"]?.jsonObject if (personalData == null) { Logger.e(TAG, "Error in German eID response data: ${germanEid.data}") diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/ProofingState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/ProofingState.kt index 7fe0637b0..33a6fa084 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/ProofingState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/ProofingState.kt @@ -3,7 +3,6 @@ package com.android.identity.issuance.hardcoded import com.android.identity.cbor.annotation.CborSerializable import com.android.identity.flow.annotation.FlowMethod import com.android.identity.flow.annotation.FlowState -import com.android.identity.flow.server.Configuration import com.android.identity.flow.server.FlowEnvironment import com.android.identity.flow.server.Storage import com.android.identity.issuance.ProofingFlow @@ -26,7 +25,6 @@ import io.ktor.client.HttpClient import io.ktor.client.request.get import io.ktor.client.statement.readBytes import kotlinx.coroutines.runBlocking -import java.net.URLEncoder /** * State of [ProofingFlow] RPC implementation. diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt index 869220371..dc49546c1 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/RequestCredentialsState.kt @@ -1,15 +1,22 @@ package com.android.identity.issuance.hardcoded import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.device.AssertionBindingKeys +import com.android.identity.device.DeviceAssertion import com.android.identity.flow.annotation.FlowMethod import com.android.identity.flow.annotation.FlowState import com.android.identity.flow.server.FlowEnvironment +import com.android.identity.flow.server.Storage import com.android.identity.issuance.CredentialConfiguration import com.android.identity.issuance.CredentialFormat import com.android.identity.issuance.CredentialRequest import com.android.identity.issuance.KeyPossessionChallenge import com.android.identity.issuance.KeyPossessionProof import com.android.identity.issuance.RequestCredentialsFlow +import com.android.identity.issuance.validateDeviceAssertionBindingKeys +import com.android.identity.issuance.wallet.ClientRecord +import com.android.identity.issuance.wallet.fromCbor +import com.android.identity.securearea.config.SecureAreaConfigurationSoftware /** * State of [RequestCredentialsFlow] RPC implementation. @@ -17,24 +24,47 @@ import com.android.identity.issuance.RequestCredentialsFlow @FlowState(flowInterface = RequestCredentialsFlow::class) @CborSerializable class RequestCredentialsState( - val documentId: String = "", - val credentialConfiguration: CredentialConfiguration? = null, - val credentialRequests: MutableList = mutableListOf(), + val clientId: String, + val documentId: String, + val credentialConfiguration: CredentialConfiguration, + val bindingKeys: MutableList = mutableListOf(), var format: CredentialFormat? = null ) { companion object {} @FlowMethod - fun getCredentialConfiguration(env: FlowEnvironment, format: CredentialFormat): CredentialConfiguration { + fun getCredentialConfiguration( + env: FlowEnvironment, + format: CredentialFormat + ): CredentialConfiguration { // TODO: make use of the format - check(credentialConfiguration != null) this.format = format return credentialConfiguration } @FlowMethod - fun sendCredentials(env: FlowEnvironment, credentialRequests: List): List { - this.credentialRequests.addAll(credentialRequests) + suspend fun sendCredentials( + env: FlowEnvironment, + credentialRequests: List, + keysAssertion: DeviceAssertion + ): List { + val storage = env.getInterface(Storage::class)!! + val clientRecord = ClientRecord.fromCbor( + storage.get("Clients", "", clientId)!!.toByteArray()) + val assertion = if (credentialConfiguration.secureAreaConfiguration is SecureAreaConfigurationSoftware) { + // if explicitly asked for software secure area, don't validate + // (it will fail), just blindly trust. + keysAssertion.assertion as AssertionBindingKeys + } else { + validateDeviceAssertionBindingKeys( + env = env, + deviceAttestation = clientRecord.deviceAttestation, + keyAttestations = credentialRequests.map { it.secureAreaBoundKeyAttestation }, + deviceAssertion = keysAssertion, + nonce = credentialConfiguration.challenge + ) + } + bindingKeys.add(assertion) return emptyList() } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/SimpleCredentialRequest.kt b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/SimpleCredentialRequest.kt index 49278a5ac..a8111a030 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/SimpleCredentialRequest.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/hardcoded/SimpleCredentialRequest.kt @@ -4,7 +4,6 @@ import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborMap import com.android.identity.crypto.EcPublicKey import com.android.identity.issuance.CredentialFormat -import kotlinx.datetime.Instant data class SimpleCredentialRequest( val authenticationKey: EcPublicKey, diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt index e8c5a07c5..0abc781bc 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/proofing/defaultGraph.kt @@ -1,10 +1,12 @@ package com.android.identity.issuance.proofing -import com.android.identity.cbor.Cbor import com.android.identity.cbor.CborMap import com.android.identity.crypto.EcCurve import com.android.identity.flow.server.Resources import com.android.identity.issuance.CredentialConfiguration +import com.android.identity.securearea.config.SecureAreaConfigurationAndroidKeystore +import com.android.identity.securearea.config.SecureAreaConfigurationCloud +import com.android.identity.securearea.config.SecureAreaConfigurationSoftware import com.android.identity.issuance.WalletApplicationCapabilities import com.android.identity.issuance.evidence.EvidenceResponse import com.android.identity.issuance.evidence.EvidenceResponseCreatePassphrase @@ -15,6 +17,7 @@ import com.android.identity.securearea.PassphraseConstraints import com.android.identity.securearea.toDataItem import kotlinx.io.bytestring.ByteString import java.net.URLEncoder +import kotlin.random.Random fun defaultGraph( documentId: String, @@ -232,19 +235,17 @@ fun defaultCredentialConfiguration( walletApplicationCapabilities: WalletApplicationCapabilities, collectedEvidence: Map ): CredentialConfiguration { - val challenge = byteArrayOf(1, 2, 3) + val challenge = ByteString(Random.nextBytes(16)) if (!collectedEvidence.containsKey("devmode_sa")) { return CredentialConfiguration( challenge, - "AndroidKeystoreSecureArea", - Cbor.encode( - CborMap.builder() - .put("curve", EcCurve.P256.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN))) - .put("userAuthenticationRequired", true) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", 3 /* LSKF + Biometrics */) - .end().build() + SecureAreaConfigurationAndroidKeystore( + purposes = KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN)), + curve = EcCurve.P256.coseCurveIdentifier, + useStrongBox = true, + userAuthenticationRequired = true, + userAuthenticationTimeoutMillis = 0, + userAuthenticationTypes = 3 // LSKF + Biometrics ) ) } @@ -299,19 +300,15 @@ fun defaultCredentialConfiguration( } return CredentialConfiguration( challenge, - "AndroidKeystoreSecureArea", - Cbor.encode( - CborMap.builder() - .put("useStrongBox", useStrongBox) - .put("userAuthenticationRequired", (userAuthType != 0)) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", userAuthType) - .put("curve", curve.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(purposes)) - .end().build() + SecureAreaConfigurationAndroidKeystore( + curve = curve.coseCurveIdentifier, + purposes = KeyPurpose.encodeSet(purposes), + useStrongBox = useStrongBox, + userAuthenticationRequired = userAuthType != 0, + userAuthenticationTimeoutMillis = 0, + userAuthenticationTypes = userAuthType.toLong(), ) ) - } "devmode_sa_software" -> { @@ -443,8 +440,7 @@ fun defaultCredentialConfiguration( } return CredentialConfiguration( challenge, - "SoftwareSecureArea", - Cbor.encode(builder.end().build()) + SecureAreaConfigurationSoftware() ) } @@ -464,15 +460,15 @@ fun defaultCredentialConfiguration( .cloudSecureAreaIdentifier return CredentialConfiguration( challenge, - cloudSecureAreaId, - Cbor.encode( - CborMap.builder() - .put("passphraseRequired", true) - .put("userAuthenticationRequired", (userAuthType != 0)) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", userAuthType) - .put("purposes", KeyPurpose.encodeSet(purposes)) - .end().build() + SecureAreaConfigurationCloud( + purposes = KeyPurpose.encodeSet(purposes), + curve = EcCurve.P256.coseCurveIdentifier, + cloudSecureAreaId = cloudSecureAreaId, + userAuthenticationTimeoutMillis = 0, + useStrongBox = true, + passphraseRequired = true, + userAuthenticationRequired = userAuthType != 0, + userAuthenticationTypes = userAuthType.toLong() ) ) } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/InProcessMrtdNfcTunnel.kt b/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/InProcessMrtdNfcTunnel.kt index 680c3a021..d1098734f 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/InProcessMrtdNfcTunnel.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/InProcessMrtdNfcTunnel.kt @@ -320,8 +320,10 @@ internal class InProcessMrtdNfcTunnel( } else { EvidenceRequestIcaoNfcTunnelType.READING } - val evidenceResponse = transmitCore(EvidenceRequestIcaoNfcTunnel( - requestType, passThrough(), progressPercent, ByteString(commandAPDU!!.bytes))) + val evidenceResponse = transmitCore( + EvidenceRequestIcaoNfcTunnel( + requestType, passThrough(), progressPercent, ByteString(commandAPDU!!.bytes)) + ) return ResponseAPDU(evidenceResponse!!.response.toByteArray()) } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/MrtdNfcTunnel.kt b/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/MrtdNfcTunnel.kt index 2ed6f2b5c..98cb4ca19 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/MrtdNfcTunnel.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/tunnel/MrtdNfcTunnel.kt @@ -1,10 +1,8 @@ package com.android.identity.issuance.tunnel import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnel -import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnelType import com.android.identity.issuance.evidence.EvidenceResponse import com.android.identity.issuance.evidence.EvidenceResponseIcaoNfcTunnel -import com.android.identity.mrtd.MrtdAccessData /** * Drives the exchange with the chip in MRTD through NFC tunnel, sending commands to the chip diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/wallet/ApplicationSupportState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/ApplicationSupportState.kt index 4df61bb90..5e5775e31 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/wallet/ApplicationSupportState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/ApplicationSupportState.kt @@ -5,7 +5,9 @@ import com.android.identity.crypto.Crypto import com.android.identity.crypto.EcPrivateKey import com.android.identity.crypto.EcPublicKey import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.javaX509Certificate +import com.android.identity.device.AssertionDPoPKey +import com.android.identity.device.DeviceAssertion +import com.android.identity.device.DeviceAttestationAndroid import com.android.identity.flow.annotation.FlowMethod import com.android.identity.flow.annotation.FlowState import com.android.identity.flow.server.Configuration @@ -16,9 +18,9 @@ import com.android.identity.issuance.LandingUrlUnknownException import com.android.identity.issuance.WalletServerSettings import com.android.identity.issuance.common.cache import com.android.identity.issuance.funke.toJson -import com.android.identity.issuance.isCloudKeyAttestation -import com.android.identity.issuance.validateCloudKeyAttestation -import com.android.identity.issuance.validateKeyAttestation +import com.android.identity.issuance.validateAndroidKeyAttestation +import com.android.identity.issuance.validateDeviceAssertion +import com.android.identity.issuance.validateDeviceAssertionBindingKeys import com.android.identity.securearea.KeyAttestation import com.android.identity.util.Logger import com.android.identity.util.toBase64Url @@ -28,6 +30,7 @@ import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @@ -41,7 +44,9 @@ class ApplicationSupportState( const val TAG = "ApplicationSupportState" // This is the ID that was allocated to our app in the context of Funke. Use it as - // default client id for ease of development. + // default OpenId4VCI client id for ease of development. It is NOT used as value for + // clientId field above! It identifies our wallet app to OpenId4VCI servers, whereas + // clientId identifies a particular wallet app instance to the wallet server. const val FUNKE_CLIENT_ID = "60f8c117-b692-4de8-8f7f-636ff852baa6" } @@ -79,20 +84,33 @@ class ApplicationSupportState( @FlowMethod suspend fun createJwtClientAssertion( - env: FlowEnvironment, attestation: KeyAttestation, targetIssuanceUrl: String + env: FlowEnvironment, + keyAttestation: KeyAttestation, + keyAssertion: DeviceAssertion ): String { - val settings = WalletServerSettings(env.getInterface(Configuration::class)!!) + val storage = env.getInterface(Storage::class)!! + val clientRecord = ClientRecord.fromCbor( + storage.get("Clients", "", clientId)!!.toByteArray()) - validateKeyAttestation( - attestation.certChain!!, - null, // no challenge check - settings.androidRequireGmsAttestation, - settings.androidRequireVerifiedBootGreen, - settings.androidRequireAppSignatureCertificateDigests - ) + validateDeviceAssertion(clientRecord.deviceAttestation, keyAssertion) - check(attestation.certChain!!.certificates[0].ecPublicKey == attestation.publicKey) - return createJwtClientAssertion(env, attestation.publicKey, targetIssuanceUrl) + val assertion = keyAssertion.assertion as AssertionDPoPKey + + if (clientRecord.deviceAttestation is DeviceAttestationAndroid) { + val settings = WalletServerSettings(env.getInterface(Configuration::class)!!) + val certChain = keyAttestation.certChain!! + check(assertion.publicKey == certChain.certificates.first().ecPublicKey) + validateAndroidKeyAttestation( + certChain, + null, // no challenge check needed + settings.androidRequireGmsAttestation, + settings.androidRequireVerifiedBootGreen, + settings.androidRequireAppSignatureCertificateDigests + ) + } + + check(keyAttestation.certChain!!.certificates[0].ecPublicKey == keyAttestation.publicKey) + return createJwtClientAssertion(env, keyAttestation.publicKey, assertion.targetUrl) } @FlowMethod @@ -104,31 +122,21 @@ class ApplicationSupportState( suspend fun createJwtKeyAttestation( env: FlowEnvironment, keyAttestations: List, - nonce: String + keysAssertion: DeviceAssertion // holds AssertionBindingKeys ): String { - val settings = WalletServerSettings(env.getInterface(Configuration::class)!!) + val storage = env.getInterface(Storage::class)!! + val clientRecord = ClientRecord.fromCbor( + storage.get("Clients", "", clientId)!!.toByteArray()) + val assertion = validateDeviceAssertionBindingKeys( + env = env, + deviceAttestation = clientRecord.deviceAttestation, + keyAttestations = keyAttestations, + deviceAssertion = keysAssertion, + nonce = null, // no check + ) - val keyList = keyAttestations.map { attestation -> - // TODO: ensure that keys come from the same device and extract data for key_type - // and user_authentication values - if (isCloudKeyAttestation(attestation.certChain!!)) { - val trustedRootKeys = getCloudSecureAreaTrustedRootKeys(env) - validateCloudKeyAttestation( - attestation.certChain!!, - nonce, - trustedRootKeys.trustedKeys - ) - } else { - validateKeyAttestation( - attestation.certChain!!, - nonce, - settings.androidRequireGmsAttestation, - settings.androidRequireVerifiedBootGreen, - settings.androidRequireAppSignatureCertificateDigests - ) - } - attestation.publicKey.toJson(null) - } + val nonce = String(assertion.nonce.toByteArray()) + val keyList = assertion.publicKeys val attestationData = env.cache(AttestationData::class) { configuration, resources -> // The key that we use here is unique for a particular Wallet ecosystem. @@ -160,17 +168,24 @@ class ApplicationSupportState( val now = Clock.System.now() val notBefore = now - 1.seconds val expiration = now + 5.minutes - val payload = JsonObject( - mapOf( - "iss" to JsonPrimitive(attestationData.clientId), - "attested_keys" to JsonArray(keyList), - "nonce" to JsonPrimitive(nonce), - "nbf" to JsonPrimitive(notBefore.epochSeconds), - "exp" to JsonPrimitive(expiration.epochSeconds), - "iat" to JsonPrimitive(now.epochSeconds) - // TODO: add appropriate key_type and user_authentication values - ) - ).toString().toByteArray().toBase64Url() + val payload = buildJsonObject { + put("iss", attestationData.clientId) + put("attested_keys", JsonArray(keyList.map { it.toJson(null) })) + put("nonce", nonce) + put("nbf", notBefore.epochSeconds) + put("exp", expiration.epochSeconds) + put("iat", now.epochSeconds) + if (assertion.userAuthentication.isNotEmpty()) { + put("user_authentication", + JsonArray(assertion.userAuthentication.map { JsonPrimitive(it) }) + ) + } + if (assertion.keyStorage.isNotEmpty()) { + put("key_storage", + JsonArray(assertion.keyStorage.map { JsonPrimitive(it) }) + ) + } + }.toString().toByteArray().toBase64Url() val message = "$head.$payload" val sig = Crypto.sign( @@ -254,26 +269,9 @@ class ApplicationSupportState( return "$message.$signature" } - private suspend fun getCloudSecureAreaTrustedRootKeys( - env: FlowEnvironment - ): CloudSecureAreaTrustedRootKeys { - return env.cache(CloudSecureAreaTrustedRootKeys::class) { configuration, resources -> - val certificateName = configuration.getValue("csa.certificate") - ?: "cloud_secure_area/certificate.pem" - val certificate = X509Cert.fromPem(resources.getStringResource(certificateName)!!) - CloudSecureAreaTrustedRootKeys( - trustedKeys = setOf(ByteString(certificate.javaX509Certificate.publicKey.encoded)) - ) - } - } - internal data class AttestationData( val certificate: X509Cert, val privateKey: EcPrivateKey, val clientId: String ) - - internal data class CloudSecureAreaTrustedRootKeys( - val trustedKeys: Set - ) } diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/wallet/AuthenticationState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/AuthenticationState.kt index b453ba697..d02827392 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/wallet/AuthenticationState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/AuthenticationState.kt @@ -1,10 +1,8 @@ package com.android.identity.issuance.wallet -import com.android.identity.cbor.Cbor import com.android.identity.cbor.annotation.CborSerializable -import com.android.identity.crypto.Algorithm -import com.android.identity.crypto.Crypto -import com.android.identity.crypto.EcPublicKey +import com.android.identity.device.AssertionNonce +import com.android.identity.device.DeviceAttestation import com.android.identity.flow.annotation.FlowMethod import com.android.identity.flow.annotation.FlowState import com.android.identity.flow.server.Configuration @@ -15,13 +13,12 @@ import com.android.identity.issuance.ClientAuthentication import com.android.identity.issuance.ClientChallenge import com.android.identity.issuance.WalletServerCapabilities import com.android.identity.issuance.WalletServerSettings -import com.android.identity.issuance.authenticationMessage -import com.android.identity.issuance.toDataItem -import com.android.identity.issuance.validateKeyAttestation +import com.android.identity.issuance.toCbor +import com.android.identity.issuance.validateDeviceAssertion +import com.android.identity.issuance.validateDeviceAttestation +import com.android.identity.util.toBase64Url import kotlinx.datetime.Clock import kotlinx.io.bytestring.ByteString -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.random.Random @FlowState(flowInterface = AuthenticationFlow::class) @@ -29,26 +26,26 @@ import kotlin.random.Random class AuthenticationState( var nonce: ByteString? = null, var clientId: String = "", - var publicKey: EcPublicKey? = null, + var deviceAttestation: DeviceAttestation? = null, var authenticated: Boolean = false ) { companion object - @OptIn(ExperimentalEncodingApi::class) @FlowMethod suspend fun requestChallenge(env: FlowEnvironment, clientId: String): ClientChallenge { check(this.clientId.isEmpty()) check(nonce != null) val storage = env.getInterface(Storage::class)!! - val keyData = storage.get("ClientKeys", "", clientId) - if (keyData != null) { - this.publicKey = EcPublicKey.fromDataItem(Cbor.decode(keyData.toByteArray())) + val clientData = storage.get("Clients", "", clientId) + if (clientData != null) { + this.deviceAttestation = + ClientRecord.fromCbor(clientData.toByteArray()).deviceAttestation this.clientId = clientId println("Existing client id: ${this.clientId}") } if (this.clientId.isEmpty()) { - this.clientId = Base64.encode(Random.Default.nextBytes(18)) + this.clientId = Random.Default.nextBytes(18).toBase64Url() println("New client id: ${this.clientId}") } @@ -57,32 +54,22 @@ class AuthenticationState( @FlowMethod suspend fun authenticate(env: FlowEnvironment, auth: ClientAuthentication): WalletServerCapabilities { - val chain = auth.attestation - val settings = WalletServerSettings(env.getInterface(Configuration::class)!!) val storage = env.getInterface(Storage::class)!! - if (chain != null) { - if (this.publicKey != null) { + val attestation = auth.attestation + if (attestation != null) { + if (this.deviceAttestation != null) { throw IllegalStateException("Client already registered") } - validateKeyAttestation( - chain, - this.clientId, - settings.androidRequireGmsAttestation, - settings.androidRequireVerifiedBootGreen, - settings.androidRequireAppSignatureCertificateDigests - ) - this.publicKey = chain.certificates[0].ecPublicKey - val keyData = ByteString(Cbor.encode(this.publicKey!!.toDataItem())) - storage.insert("ClientKeys", "", keyData, key = clientId) + validateDeviceAttestation(attestation, clientId, settings) + val clientData = ByteString(ClientRecord(attestation).toCbor()) + this.deviceAttestation = attestation + storage.insert("Clients", "", clientData, key = clientId) } - if (!Crypto.checkSignature( - this.publicKey!!, - authenticationMessage(this.clientId, this.nonce!!).toByteArray(), - Algorithm.ES256, - auth.signature)) { - throw IllegalArgumentException("Authentication failed") + validateDeviceAssertion(this.deviceAttestation!!, auth.assertion) + if ((auth.assertion.assertion as AssertionNonce).nonce != this.nonce) { + throw IllegalArgumentException("nonce mismatch") } authenticated = true if (storage.get( @@ -93,7 +80,7 @@ class AuthenticationState( storage.insert( "WalletApplicationCapabilities", "", - ByteString(Cbor.encode(auth.walletApplicationCapabilities.toDataItem())), + ByteString(auth.walletApplicationCapabilities.toCbor()), clientId ) } else { @@ -101,7 +88,7 @@ class AuthenticationState( "WalletApplicationCapabilities", "", clientId, - ByteString(Cbor.encode(auth.walletApplicationCapabilities.toDataItem())) + ByteString(auth.walletApplicationCapabilities.toCbor()) ) } return WalletServerCapabilities( diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/wallet/ClientRecord.kt b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/ClientRecord.kt new file mode 100644 index 000000000..b7394aa9f --- /dev/null +++ b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/ClientRecord.kt @@ -0,0 +1,19 @@ +package com.android.identity.issuance.wallet + +import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.device.DeviceAttestation + +/** + * Data that we keep for each device that ever connected to this wallet server. + * + * It is created and stored (identified by the `clientId`) the first time the new device is seen. + * When the device wishes to connect to the server again, it must prove that it still possesses the + * private key that was used initially. If the key is lost (e.g. wallet app is moved to another + * device or its storage erased), the app is treated as a new client. + */ +@CborSerializable +data class ClientRecord( + val deviceAttestation: DeviceAttestation +) { + companion object +} \ No newline at end of file diff --git a/identity-issuance/src/main/java/com/android/identity/issuance/wallet/WalletServerState.kt b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/WalletServerState.kt index b46349f87..947ade54e 100644 --- a/identity-issuance/src/main/java/com/android/identity/issuance/wallet/WalletServerState.kt +++ b/identity-issuance/src/main/java/com/android/identity/issuance/wallet/WalletServerState.kt @@ -10,7 +10,6 @@ import com.android.identity.flow.server.Configuration import com.android.identity.flow.server.FlowEnvironment import com.android.identity.flow.server.Resources import com.android.identity.issuance.ApplicationSupport -import com.android.identity.issuance.CredentialFormat import com.android.identity.issuance.DocumentConfiguration import com.android.identity.issuance.IssuingAuthorityConfiguration import com.android.identity.issuance.IssuingAuthorityException diff --git a/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift b/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift index 74497c802..f0056f19d 100644 --- a/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift +++ b/identity/SwiftBridge/SwiftBridge/SwiftBridge.swift @@ -2,6 +2,7 @@ import CryptoKit import Foundation import Security import LocalAuthentication +import DeviceCheck @objc public class SwiftBridge : NSObject { @objc(sha256:) public class func sha256(data: Data) -> Data { @@ -365,5 +366,53 @@ import LocalAuthentication let data = SecKeyCopyExternalRepresentation(key!, nil) return data as Data? } + + @objc(generateDeviceAttestation::) public class func generateDeviceAttestation( + dataHash: Data, + completionHandler: @escaping (String?, Data?, Error?) -> Void + ) -> Void { + let attestService = DCAppAttestService.shared + guard attestService.isSupported == true else { + completionHandler(nil, nil, NSError(domain: "com.android.identity", code: 1, userInfo: [ + "message": "This device does not support attestation" + ])) + return + } + + guard dataHash.count == 32 else { + print("Invalid dataHash size") + completionHandler(nil, nil, NSError(domain: "com.android.identity", code: 2, userInfo: [ + "message": "dataHash length must be 32 bytes" + ])) + return + } + + attestService.generateKey { keyId, err in + guard err == nil else { + completionHandler(nil, nil, err) + return + } + + attestService.attestKey(keyId!, clientDataHash: dataHash) { attestation, err in + guard err == nil else { + completionHandler(nil, nil, err) + return + } + completionHandler(keyId!, attestation, nil) + } + } + } + + @objc(generateDeviceAssertion:::) public class func generateDeviceAssertion( + keyId: String, + dataHash: Data, + completionHandler: @escaping (Data?, Error?) -> Void + ) -> Void { + DCAppAttestService.shared.generateAssertion( + keyId, + clientDataHash: dataHash, + completionHandler: completionHandler + ) + } } diff --git a/identity/build.gradle.kts b/identity/build.gradle.kts index f6e7dbb88..dec851d05 100644 --- a/identity/build.gradle.kts +++ b/identity/build.gradle.kts @@ -59,6 +59,9 @@ kotlin { } } + // we want some extra dependsOn calls to create + // javaSharedMain to share between JVM and Android, + // but otherwise want to follow default hierarchy. applyDefaultHierarchyTemplate() sourceSets { @@ -82,7 +85,17 @@ kotlin { } } + val javaSharedMain by creating { + dependsOn(commonMain) + dependencies { + implementation(libs.bouncy.castle.bcprov) + implementation(libs.bouncy.castle.bcpkix) + implementation(libs.tink) + } + } + val jvmMain by getting { + dependsOn(javaSharedMain) dependencies { implementation(libs.bouncy.castle.bcprov) implementation(libs.bouncy.castle.bcpkix) @@ -91,14 +104,13 @@ kotlin { } val androidMain by getting { - dependsOn(jvmMain) + dependsOn(javaSharedMain) dependencies { implementation(libs.bouncy.castle.bcprov) implementation(libs.bouncy.castle.bcpkix) implementation(libs.tink) } } - } } @@ -150,6 +162,28 @@ android { group = "com.android.identity" version = projectVersionName +android { + namespace = "com.android.identity" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + minSdk = 26 + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + packaging { + resources { + excludes += listOf("/META-INF/{AL2.0,LGPL2.1}") + excludes += listOf("/META-INF/versions/9/OSGI-INF/MANIFEST.MF") + } + } +} + publishing { repositories { maven { diff --git a/identity/src/androidMain/kotlin/com/android/identity/device/DeviceCheck.android.kt b/identity/src/androidMain/kotlin/com/android/identity/device/DeviceCheck.android.kt new file mode 100644 index 000000000..10d7d81d0 --- /dev/null +++ b/identity/src/androidMain/kotlin/com/android/identity/device/DeviceCheck.android.kt @@ -0,0 +1,46 @@ +package com.android.identity.device + +import com.android.identity.crypto.Algorithm +import com.android.identity.securearea.CreateKeySettings +import com.android.identity.securearea.SecureArea +import com.android.identity.util.toBase64Url +import kotlinx.io.bytestring.ByteString +import kotlin.random.Random + +/** + * Generates statements validating device/app/OS integrity. Details of these + * statements are inherently platform-specific. + */ +actual object DeviceCheck { + actual suspend fun generateAttestation( + secureArea: SecureArea, + clientId: String + ): DeviceAttestationResult { + val alias = "deviceCheck_" + Random.nextBytes(9).toBase64Url() + // TODO: utilize clientId once we have access to AndroidSecureArea APIs here + // and start checking it on the server + secureArea.createKey(alias, CreateKeySettings()) + val keyInfo = secureArea.getKeyInfo(alias) + return DeviceAttestationResult( + deviceAttestationId = alias, + deviceAttestation = DeviceAttestationAndroid(keyInfo.attestation.certChain!!) + ) + } + + actual suspend fun generateAssertion( + secureArea: SecureArea, + deviceAttestationId: String, + assertion: Assertion + ): DeviceAssertion { + val assertionData = assertion.toCbor() + val signature = secureArea.sign( + alias = deviceAttestationId, + signatureAlgorithm = Algorithm.ES256, + dataToSign = assertionData, + keyUnlockData = null) + return DeviceAssertion( + assertionData = ByteString(assertionData), + platformAssertion = ByteString(signature.toCoseEncoded()) + ) + } +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/Assertion.kt b/identity/src/commonMain/kotlin/com/android/identity/device/Assertion.kt new file mode 100644 index 000000000..7a500937b --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/Assertion.kt @@ -0,0 +1,12 @@ +package com.android.identity.device + +import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.securearea.KeyAttestation + +/** + * An open-ended statement that can be wrapped in [DeviceAssertion]. + */ +@CborSerializable +sealed class Assertion { + companion object +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/AssertionBindingKeys.kt b/identity/src/commonMain/kotlin/com/android/identity/device/AssertionBindingKeys.kt new file mode 100644 index 000000000..b690b6a36 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/AssertionBindingKeys.kt @@ -0,0 +1,32 @@ +package com.android.identity.device + +import com.android.identity.crypto.EcPublicKey +import kotlinx.datetime.Instant +import kotlinx.io.bytestring.ByteString + +/** + * Asserts that the given list of public keys corresponds to the freshly-minted + * private keys on the device with the given properties. + * + * [clientId] and [nonce] are provided by the server. [nonce] is provided immediately before + * the key is created and it asserts freshness. [clientId] is provided during server/client + * initial handshake and it asserts that the private key resides on a particular device known + * to the server. + * + * [keyStorage] and [userAuthentication] are as defined OpenID4VCI key attestation `key_storage` + * and `user-authentication` field. [issuedAt] and [expiration] semantics corresponds to + * `iat` and `exp` fields in JWT. + * + * TODO: in addition to values defined in the OpenID4VCI spec, select well-defined values + * for `key_storage` and `user-authentication` that directly correspond to what is available on + * the platforms that we support and list them here. + */ +data class AssertionBindingKeys( + val publicKeys: List, + val nonce: ByteString, + val clientId: String, + val keyStorage: List, + val userAuthentication: List, + val issuedAt: Instant, + val expiration: Instant? = null, +): Assertion() \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/AssertionDPoPKey.kt b/identity/src/commonMain/kotlin/com/android/identity/device/AssertionDPoPKey.kt new file mode 100644 index 000000000..57e6f897e --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/AssertionDPoPKey.kt @@ -0,0 +1,13 @@ +package com.android.identity.device + +import com.android.identity.crypto.EcPublicKey + +/** + * Asserts that the device possesses private key for the given public key. + * + * The private that is then used for DPoP authorizations for communication with the issuance server. + */ +data class AssertionDPoPKey( + val publicKey: EcPublicKey, + val targetUrl: String +) : Assertion() \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/AssertionNonce.kt b/identity/src/commonMain/kotlin/com/android/identity/device/AssertionNonce.kt new file mode 100644 index 000000000..0dffd1e60 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/AssertionNonce.kt @@ -0,0 +1,11 @@ +package com.android.identity.device + +import kotlinx.io.bytestring.ByteString + +/** + * Asserts that the device has access to the opaque private key in [DeviceAttestation] by signing + * server-supplied [nonce]. + */ +class AssertionNonce ( + val nonce: ByteString, +): Assertion() \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertion.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertion.kt new file mode 100644 index 000000000..116ffa5fc --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertion.kt @@ -0,0 +1,34 @@ +package com.android.identity.device + +import com.android.identity.cbor.annotation.CborSerializable +import kotlinx.io.bytestring.ByteString + +/** + * [Assertion] and additional data that can be used to validate the statement in the assertion. + * + * [DeviceAssertion] validation requires access to the corresponding [DeviceAttestation]. + * + * Note that unlike [DeviceAttestation], [DeviceAssertion] is vouched for by the wallet app + * (so it is important that [DeviceAttestation] was validated at some point, as it is the + * [DeviceAttestation] which is the expression of the platform vouching for the wallet app). + */ +@CborSerializable +data class DeviceAssertion( + /** + * Cbor-serialized [Assertion], signed over by platformAssertion. + * + * This is kept serialized, so that serialization discrepancies do not affect our ability + * to validate [platformAssertion]. + */ + val assertionData: ByteString, + + /** + * Platform-specific "signature" validating integrity of the [assertionData]. + */ + val platformAssertion: ByteString, +) { + val assertion: Assertion + get() = Assertion.fromCbor(assertionData.toByteArray()) + + companion object +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertionMaker.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertionMaker.kt new file mode 100644 index 000000000..f3655cf55 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAssertionMaker.kt @@ -0,0 +1,5 @@ +package com.android.identity.device + +fun interface DeviceAssertionMaker { + suspend fun makeDeviceAssertion(assertion: Assertion): DeviceAssertion +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestation.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestation.kt new file mode 100644 index 000000000..5aaa7d988 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestation.kt @@ -0,0 +1,14 @@ +package com.android.identity.device + +import com.android.identity.cbor.annotation.CborSerializable + +/** + * A platform-issued statement vouching for the integrity of the wallet app. + * + * A device attestation can be validated on server (which is **not** running on the platform + * that produced [DeviceAttestation]). + */ +@CborSerializable +sealed class DeviceAttestation { + companion object +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationAndroid.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationAndroid.kt new file mode 100644 index 000000000..a4a7024d4 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationAndroid.kt @@ -0,0 +1,11 @@ +package com.android.identity.device + +import com.android.identity.crypto.X509CertChain + +/** + * On Android we create a private key in secure area and use its key attestation as the + * device attestation. + */ +data class DeviceAttestationAndroid( + val certificateChain: X509CertChain +) : DeviceAttestation() \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationIos.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationIos.kt new file mode 100644 index 000000000..073e00248 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationIos.kt @@ -0,0 +1,8 @@ +package com.android.identity.device + +import kotlinx.io.bytestring.ByteString + +/** On iOS device attestation is the result of Apple's DeviceCheck API. */ +data class DeviceAttestationIos( + val blob: ByteString +): DeviceAttestation() \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationJvm.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationJvm.kt new file mode 100644 index 000000000..766cfb545 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationJvm.kt @@ -0,0 +1,4 @@ +package com.android.identity.device + +/** Plain JVM does not have a way to generate a device attestation. */ +class DeviceAttestationJvm() : DeviceAttestation() \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationResult.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationResult.kt new file mode 100644 index 000000000..a955d2855 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceAttestationResult.kt @@ -0,0 +1,14 @@ +package com.android.identity.device + +data class DeviceAttestationResult( + /** + * Identifier for the opaque private key generated by [DeviceCheck.generateAttestation] + * method + */ + val deviceAttestationId: String, + + /** + * Data that proves the platform integrity. + */ + val deviceAttestation: DeviceAttestation +) \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/device/DeviceCheck.kt b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceCheck.kt new file mode 100644 index 000000000..230edcef8 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/device/DeviceCheck.kt @@ -0,0 +1,39 @@ +package com.android.identity.device + +import com.android.identity.securearea.SecureArea +import kotlinx.io.bytestring.ByteString + +/** + * Generates statements validating device/OS/app integrity. Details of these + * statements are inherently platform-specific. + */ +expect object DeviceCheck { + /** + * Generates a device attestation that proves the integrity of the device/OS/wallet app + * and creates a certain opaque private key that resides securely on the device. + * + * The only operation that this opaque private key can be used for is generating + * assertions using [generateAssertion] method. + * + * [secureArea] must be platform-specific [SecureArea] but it may or may not be used + * by this method depending on the platform. + */ + suspend fun generateAttestation( + secureArea: SecureArea, + clientId: String + ): DeviceAttestationResult + + /** + * Generates [DeviceAssertion] - an [Assertion] which is signed using the key generated using + * by [generateAttestation] method. + * + * Note that the exact format for the signature is platform-dependent. + * + * [secureArea] must be the same value as was passed to [generateAttestation] method. + */ + suspend fun generateAssertion( + secureArea: SecureArea, + deviceAttestationId: String, + assertion: Assertion + ): DeviceAssertion +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfiguration.kt b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfiguration.kt new file mode 100644 index 000000000..5c077db3f --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfiguration.kt @@ -0,0 +1,19 @@ +package com.android.identity.securearea.config + +import com.android.identity.cbor.annotation.CborSerializable +import com.android.identity.crypto.EcCurve +import com.android.identity.securearea.KeyPurpose +import com.android.identity.securearea.SecureArea + +/** + * Configuration for a specific secure area [SecureArea] to use. + */ +@CborSerializable +sealed class SecureAreaConfiguration( + /** The value is a number encoded like in [KeyPurpose.encodeSet] */ + val purposes: Long, + /** The value is a number encoded like [EcCurve.coseCurveIdentifier] */ + val curve: Int +) { + companion object +} \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationAndroidKeystore.kt b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationAndroidKeystore.kt new file mode 100644 index 000000000..745607e30 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationAndroidKeystore.kt @@ -0,0 +1,15 @@ +package com.android.identity.securearea.config + +/** Secure area configuration for [AndroidKeystoreSecureArea] */ +class SecureAreaConfigurationAndroidKeystore( + purposes: Long, + curve: Int, + /** true to use StrongBox, false otherwise */ + val useStrongBox: Boolean, + /** whether to require user authentication */ + val userAuthenticationRequired: Boolean, + /** User authentication timeout in milliseconds or 0 to require authentication on every use. */ + val userAuthenticationTimeoutMillis: Long, + /** number like in [UserAuthenticationType.encodeSet] */ + val userAuthenticationTypes: Long, +) : SecureAreaConfiguration(purposes, curve) diff --git a/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationCloud.kt b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationCloud.kt new file mode 100644 index 000000000..d0985e3e7 --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationCloud.kt @@ -0,0 +1,17 @@ +package com.android.identity.securearea.config + +class SecureAreaConfigurationCloud( + purposes: Long, + curve: Int, + /** Cloud secure area id */ + val cloudSecureAreaId: String, + /** whether to require user authentication */ + val userAuthenticationRequired: Boolean, + val useStrongBox: Boolean, + /** User authentication timeout in milliseconds or 0 to require authentication on every use. */ + val userAuthenticationTimeoutMillis: Long, + /** a number like in [UserAuthenticationType.encodeSet] */ + val userAuthenticationTypes: Long, + /** whether to require passphrase authentication */ + val passphraseRequired: Boolean +): SecureAreaConfiguration(purposes, curve) \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationSoftware.kt b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationSoftware.kt new file mode 100644 index 000000000..0a4b4f34a --- /dev/null +++ b/identity/src/commonMain/kotlin/com/android/identity/securearea/config/SecureAreaConfigurationSoftware.kt @@ -0,0 +1,16 @@ +package com.android.identity.securearea.config + +import com.android.identity.crypto.EcCurve +import com.android.identity.securearea.KeyPurpose +import com.android.identity.securearea.PassphraseConstraints +import com.android.identity.securearea.software.SoftwareSecureArea + +/** + * Configuration for [SoftwareSecureArea] + */ +class SecureAreaConfigurationSoftware( + purposes: Long = KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN)), + curve: Int = EcCurve.P256.coseCurveIdentifier, + val passphrase: String? = null, + val passphraseConstraints: PassphraseConstraints = PassphraseConstraints.NONE +): SecureAreaConfiguration(purposes, curve) \ No newline at end of file diff --git a/identity/src/commonMain/kotlin/com/android/identity/securearea/software/SoftwareCreateKeySettings.kt b/identity/src/commonMain/kotlin/com/android/identity/securearea/software/SoftwareCreateKeySettings.kt index 5bb0eb66f..57ae94e8d 100644 --- a/identity/src/commonMain/kotlin/com/android/identity/securearea/software/SoftwareCreateKeySettings.kt +++ b/identity/src/commonMain/kotlin/com/android/identity/securearea/software/SoftwareCreateKeySettings.kt @@ -1,11 +1,10 @@ package com.android.identity.securearea.software -import com.android.identity.cbor.DataItem import com.android.identity.crypto.EcCurve import com.android.identity.securearea.CreateKeySettings import com.android.identity.securearea.KeyPurpose import com.android.identity.securearea.PassphraseConstraints -import com.android.identity.securearea.fromDataItem +import com.android.identity.securearea.config.SecureAreaConfigurationSoftware import kotlinx.datetime.Instant; /** @@ -43,25 +42,14 @@ class SoftwareCreateKeySettings internal constructor( * @param configuration configuration from a CBOR map. * @return the builder. */ - fun applyConfiguration(configuration: DataItem) = apply { - var passphraseRequired = false - var passphrase: String? = null - var passphraseConstraints: PassphraseConstraints? = null - for ((key, value) in configuration.asMap) { - when (key.asTstr) { - "purposes" -> setKeyPurposes(KeyPurpose.decodeSet(value.asNumber)) - "curve" -> setEcCurve(EcCurve.fromInt(value.asNumber.toInt())) - "passphrase" -> { - passphraseRequired = true - passphrase = value.asTstr - } - "passphraseConstraints" -> { - passphraseRequired = true - passphraseConstraints = PassphraseConstraints.fromDataItem(value) - } - } - } - setPassphraseRequired(passphraseRequired, passphrase, passphraseConstraints) + fun applyConfiguration(configuration: SecureAreaConfigurationSoftware) = apply { + setKeyPurposes(KeyPurpose.decodeSet(configuration.purposes)) + setEcCurve(EcCurve.fromInt(configuration.curve)) + setPassphraseRequired( + required = configuration.passphrase != null, + passphrase = configuration.passphrase, + constraints = configuration.passphraseConstraints + ) } /** diff --git a/identity/src/iosMain/kotlin/com/android/identity/device/DeviceCheck.ios.kt b/identity/src/iosMain/kotlin/com/android/identity/device/DeviceCheck.ios.kt new file mode 100644 index 000000000..261afa88b --- /dev/null +++ b/identity/src/iosMain/kotlin/com/android/identity/device/DeviceCheck.ios.kt @@ -0,0 +1,83 @@ +package com.android.identity.device + +import com.android.identity.SwiftBridge +import com.android.identity.crypto.Algorithm +import com.android.identity.crypto.Crypto +import com.android.identity.securearea.SecureArea +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.allocArrayOf +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.usePinned +import kotlinx.io.bytestring.ByteString +import platform.Foundation.NSData +import platform.Foundation.create +import platform.posix.memcpy +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@OptIn(ExperimentalForeignApi::class) +actual object DeviceCheck { + actual suspend fun generateAttestation( + secureArea: SecureArea, + clientId: String + ): DeviceAttestationResult { + val nonce = Crypto.digest(Algorithm.SHA256, clientId.encodeToByteArray()) + return suspendCoroutine { continuation -> + SwiftBridge.generateDeviceAttestation(nonce.toNSData()) { keyId, blob, err -> + if (err != null) { + continuation.resumeWithException(Exception()) + } else { + continuation.resume(DeviceAttestationResult( + keyId!!, + DeviceAttestationIos(blob!!.toByteString()) + )) + } + } + } + } + + actual suspend fun generateAssertion( + secureArea: SecureArea, + deviceAttestationId: String, + assertion: Assertion + ): DeviceAssertion { + return suspendCoroutine { continuation -> + val assertionData = assertion.toCbor() + val digest = Crypto.digest(Algorithm.SHA256, assertionData) + SwiftBridge.generateDeviceAssertion( + deviceAttestationId, + digest.toNSData() + ) { blob, err -> + if (err != null) { + continuation.resumeWithException(Exception()) + } else { + continuation.resume( + DeviceAssertion( + platformAssertion = blob!!.toByteString(), + assertionData = ByteString(assertionData) + ) + ) + } + } + } + } +} + +private fun ByteString.toNSData(): NSData = toByteArray().toNSData() + +@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) +private fun ByteArray.toNSData(): NSData = memScoped { + NSData.create(bytes = allocArrayOf(this@toNSData), length = size.toULong()) +} + +@OptIn(ExperimentalForeignApi::class) +internal fun NSData.toByteString(): ByteString { + return ByteString(ByteArray(length.toInt()).apply { + usePinned { + memcpy(it.addressOf(0), bytes, length) + } + }) +} diff --git a/identity/src/jvmMain/kotlin/com/android/identity/crypto/CryptoJvm.kt b/identity/src/javasharedMain/kotlin/com/android/identity/crypto/CryptoJvm.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/crypto/CryptoJvm.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/crypto/CryptoJvm.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/crypto/EcPrivateKeyJvm.kt b/identity/src/javasharedMain/kotlin/com/android/identity/crypto/EcPrivateKeyJvm.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/crypto/EcPrivateKeyJvm.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/crypto/EcPrivateKeyJvm.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/crypto/EcPublicKeyJvm.kt b/identity/src/javasharedMain/kotlin/com/android/identity/crypto/EcPublicKeyJvm.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/crypto/EcPublicKeyJvm.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/crypto/EcPublicKeyJvm.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt b/identity/src/javasharedMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/crypto/EcSignatureJvm.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/crypto/X509CertChainJvm.kt b/identity/src/javasharedMain/kotlin/com/android/identity/crypto/X509CertChainJvm.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/crypto/X509CertChainJvm.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/crypto/X509CertChainJvm.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/crypto/X509CertJvm.kt b/identity/src/javasharedMain/kotlin/com/android/identity/crypto/X509CertJvm.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/crypto/X509CertJvm.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/crypto/X509CertJvm.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt b/identity/src/javasharedMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/trustmanagement/TrustManager.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt b/identity/src/javasharedMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/trustmanagement/TrustManagerUtil.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/util/AndroidAttestationExtensionParser.kt b/identity/src/javasharedMain/kotlin/com/android/identity/util/AndroidAttestationExtensionParser.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/util/AndroidAttestationExtensionParser.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/util/AndroidAttestationExtensionParser.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/util/UUIDJvm.kt b/identity/src/javasharedMain/kotlin/com/android/identity/util/UUIDJvm.kt similarity index 100% rename from identity/src/jvmMain/kotlin/com/android/identity/util/UUIDJvm.kt rename to identity/src/javasharedMain/kotlin/com/android/identity/util/UUIDJvm.kt diff --git a/identity/src/jvmMain/kotlin/com/android/identity/device/DeviceCheck.jvm.kt b/identity/src/jvmMain/kotlin/com/android/identity/device/DeviceCheck.jvm.kt new file mode 100644 index 000000000..aac4af494 --- /dev/null +++ b/identity/src/jvmMain/kotlin/com/android/identity/device/DeviceCheck.jvm.kt @@ -0,0 +1,28 @@ +package com.android.identity.device + +import com.android.identity.securearea.SecureArea +import kotlinx.io.bytestring.ByteString + +/** + * Generates statements validating device/app/OS integrity. Details of these + * statements are inherently platform-specific. + */ +actual object DeviceCheck { + actual suspend fun generateAttestation( + secureArea: SecureArea, + clientId: String + ): DeviceAttestationResult { + return DeviceAttestationResult( + "", + DeviceAttestationJvm() + ) + } + + actual suspend fun generateAssertion( + secureArea: SecureArea, + deviceAttestationId: String, + assertion: Assertion + ): DeviceAssertion { + return DeviceAssertion(ByteString(), ByteString(assertion.toCbor())) + } +} \ No newline at end of file diff --git a/processor/src/main/kotlin/com/android/identity/processor/CborSymbolProcessor.kt b/processor/src/main/kotlin/com/android/identity/processor/CborSymbolProcessor.kt index 9346bc8b8..55f8389f4 100644 --- a/processor/src/main/kotlin/com/android/identity/processor/CborSymbolProcessor.kt +++ b/processor/src/main/kotlin/com/android/identity/processor/CborSymbolProcessor.kt @@ -174,8 +174,9 @@ class CborSymbolProcessor( type: KSType ): String { val declaration = type.declaration - val qualifiedName = declaration.qualifiedName!!.asString() - when (qualifiedName) { + val declarationQualifiedName = declaration.qualifiedName + ?: throw NullPointerException("Declaration $declaration with null qualified name!") + when (val qualifiedName = declarationQualifiedName.asString()) { "kotlin.collections.Map", "kotlin.collections.MutableMap" -> with(codeBuilder) { val map = varName("map") @@ -458,6 +459,9 @@ class CborSymbolProcessor( line("builder.put(\"$typeKey\", \"$typeId\")") } classDeclaration.getAllProperties().forEach { property -> + if (!property.hasBackingField) { + return@forEach + } val name = property.simpleName.asString() val type = property.type.resolve() // We want exceptions to be serializable (if marked with @CborSerializable), @@ -502,6 +506,9 @@ class CborSymbolProcessor( block("fun $deserializer($dataItem: DataItem): $baseName") { val constructorParameters = mutableListOf() classDeclaration.getAllProperties().forEach { property -> + if (!property.hasBackingField) { + return@forEach + } val type = property.type.resolve() val fieldName = property.simpleName.asString() // We want exceptions to be serializable (if marked with @CborSerializable), @@ -550,6 +557,9 @@ class CborSymbolProcessor( line("private val $fieldNameSet = setOf(") withIndent { classDeclaration.getAllProperties().forEach { property -> + if (!property.hasBackingField) { + return@forEach + } val name = property.simpleName.asString() line("\"$name\",") } diff --git a/processor/src/main/kotlin/com/android/identity/processor/FlowSymbolProcessor.kt b/processor/src/main/kotlin/com/android/identity/processor/FlowSymbolProcessor.kt index 0396f8082..0e644bf61 100644 --- a/processor/src/main/kotlin/com/android/identity/processor/FlowSymbolProcessor.kt +++ b/processor/src/main/kotlin/com/android/identity/processor/FlowSymbolProcessor.kt @@ -383,6 +383,9 @@ class FlowSymbolProcessor( operations: List ) { val lastDotImpl = flowImplName.lastIndexOf('.') + if (lastDotImpl < 0) { + throw IllegalArgumentException("Expected fully-qualified name: $flowImplName ($interfaceFullName)") + } val packageName = flowImplName.substring(0, lastDotImpl) val baseName = flowImplName.substring(lastDotImpl + 1) with(CodeBuilder()) { @@ -834,7 +837,7 @@ class FlowSymbolProcessor( annotation?.arguments?.forEach { arg -> if (arg.name?.asString() == name) { val field = arg.value.toString() - if (field.isNotEmpty()) { + if (field.isNotEmpty() && field != "null") { return field } } diff --git a/samples/testapp/build.gradle.kts b/samples/testapp/build.gradle.kts index 78c0d8d46..3137e3c0d 100644 --- a/samples/testapp/build.gradle.kts +++ b/samples/testapp/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.jetbrainsCompose) alias(libs.plugins.compose.compiler) alias(libs.plugins.buildconfig) + alias(libs.plugins.ksp) } val projectVersionCode: Int by rootProject.extra @@ -36,42 +37,71 @@ kotlin { isStatic = true } } - + + applyDefaultHierarchyTemplate() + sourceSets { + val iosMain by getting { + dependencies { + implementation(libs.ktor.client.darwin) + } + } - iosMain.dependencies { - implementation(libs.ktor.client.darwin) + val iosX64Main by getting { + dependencies {} } - androidMain.dependencies { - implementation(compose.preview) - implementation(libs.androidx.activity.compose) - implementation(libs.bouncy.castle.bcprov) - implementation(libs.androidx.biometrics) - implementation(libs.ktor.client.android) - implementation(project(":identity-android")) - implementation(project(":identity-android-csa")) + val iosArm64Main by getting { + dependencies {} } - commonMain.dependencies { - implementation(compose.runtime) - implementation(compose.foundation) - implementation(compose.material3) - implementation(compose.ui) - implementation(compose.components.resources) - implementation(compose.components.uiToolingPreview) - implementation(compose.materialIconsExtended) - implementation(libs.jetbrains.navigation.compose) - implementation(libs.jetbrains.navigation.runtime) - implementation(libs.jetbrains.lifecycle.viewmodel.compose) - implementation(libs.ktor.client.core) - implementation(libs.ktor.network) - implementation(project(":identity")) - implementation(project(":identity-mdoc")) - implementation(project(":identity-appsupport")) - implementation(project(":identity-doctypes")) - implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.io.core) + val iosSimulatorArm64Main by getting { + dependencies {} + } + + val androidMain by getting { + dependencies { + implementation(compose.preview) + implementation(libs.androidx.activity.compose) + implementation(libs.bouncy.castle.bcprov) + implementation(libs.androidx.biometrics) + implementation(libs.ktor.client.android) + implementation(project(":identity-android")) + implementation(project(":identity-android-csa")) + } + } + + val commonMain by getting { + kotlin.srcDir("build/generated/ksp/metadata/commonMain/kotlin") + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(compose.materialIconsExtended) + implementation(libs.jetbrains.navigation.compose) + implementation(libs.jetbrains.navigation.runtime) + implementation(libs.jetbrains.lifecycle.viewmodel.compose) + implementation(libs.ktor.client.core) + implementation(libs.ktor.network) + implementation(projects.processorAnnotations) + + implementation(project(":identity")) + implementation(project(":identity-mdoc")) + implementation(project(":identity-appsupport")) + implementation(project(":identity-doctypes")) + implementation(project(":identity-flow")) + implementation(project(":identity-issuance-api")) + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.io.core) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + implementation(libs.kotlinx.serialization.json) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + } } } } @@ -120,3 +150,17 @@ android { debugImplementation(compose.uiTooling) } } + +dependencies { + add("kspCommonMainMetadata", project(":processor")) +} + +tasks.withType().all { + if (name != "kspCommonMainKotlinMetadata") { + dependsOn("kspCommonMainKotlinMetadata") + } +} + +tasks["compileKotlinIosX64"].dependsOn("kspCommonMainKotlinMetadata") +tasks["compileKotlinIosArm64"].dependsOn("kspCommonMainKotlinMetadata") +tasks["compileKotlinIosSimulatorArm64"].dependsOn("kspCommonMainKotlinMetadata") diff --git a/samples/testapp/iosApp/TestApp.xcodeproj/project.pbxproj b/samples/testapp/iosApp/TestApp.xcodeproj/project.pbxproj index 99a8bbdf4..0c17b57b6 100644 --- a/samples/testapp/iosApp/TestApp.xcodeproj/project.pbxproj +++ b/samples/testapp/iosApp/TestApp.xcodeproj/project.pbxproj @@ -317,7 +317,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"testApp/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 74HWMG89B3; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -329,8 +329,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.android.identity.testapp; + PRODUCT_BUNDLE_IDENTIFIER = com.sorotokin.identity.testapp; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -348,7 +347,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"testApp/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 74HWMG89B3; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -360,8 +359,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; - "PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = com.android.identity.testapp; + PRODUCT_BUNDLE_IDENTIFIER = com.sorotokin.identity.testapp; PRODUCT_NAME = "${APP_NAME}"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; diff --git a/samples/testapp/iosApp/TestApp/TestApp.entitlements b/samples/testapp/iosApp/TestApp/TestApp.entitlements index 70084b416..0c67376eb 100644 --- a/samples/testapp/iosApp/TestApp/TestApp.entitlements +++ b/samples/testapp/iosApp/TestApp/TestApp.entitlements @@ -1,8 +1,5 @@ - - com.apple.developer.devicecheck.appattest-environment - development - + diff --git a/samples/testapp/src/androidMain/kotlin/com/android/identity/testapp/PlatformAndroid.kt b/samples/testapp/src/androidMain/kotlin/com/android/identity/testapp/PlatformAndroid.kt index 68b4d7a96..e3684ebe6 100644 --- a/samples/testapp/src/androidMain/kotlin/com/android/identity/testapp/PlatformAndroid.kt +++ b/samples/testapp/src/androidMain/kotlin/com/android/identity/testapp/PlatformAndroid.kt @@ -1,5 +1,12 @@ package com.android.identity.testapp +import android.os.Build +import com.android.identity.android.securearea.AndroidKeystoreCreateKeySettings +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.android.storage.AndroidStorageEngine +import com.android.identity.securearea.CreateKeySettings +import com.android.identity.securearea.SecureArea +import kotlinx.io.files.Path import java.net.NetworkInterface actual val platform = Platform.ANDROID @@ -17,3 +24,58 @@ actual fun getLocalIpAddress(): String { } throw IllegalStateException("Unable to determine address") } + +private val androidKeystoreStorage: AndroidStorageEngine by lazy { + AndroidStorageEngine.Builder( + MainActivity.appContext, + Path(MainActivity.appContext.dataDir.path, "testapp-default.bin") + ).build() +} + +private val androidKeystoreSecureArea: AndroidKeystoreSecureArea by lazy { + AndroidKeystoreSecureArea(MainActivity.appContext, androidKeystoreStorage) +} + +actual fun platformSecureArea(): SecureArea { + return androidKeystoreSecureArea +} + +actual fun platformKeySetting(clientId: String): CreateKeySettings { + return AndroidKeystoreCreateKeySettings.Builder(clientId.toByteArray()).build() +} + +// https://stackoverflow.com/a/21505193/878126 +actual val platformIsEmulator: Boolean by lazy { + // Android SDK emulator + return@lazy ((Build.MANUFACTURER == "Google" && Build.BRAND == "google" && + ((Build.FINGERPRINT.startsWith("google/sdk_gphone_") + && Build.FINGERPRINT.endsWith(":user/release-keys") + && Build.PRODUCT.startsWith("sdk_gphone_") + && Build.MODEL.startsWith("sdk_gphone_")) + //alternative + || (Build.FINGERPRINT.startsWith("google/sdk_gphone64_") + && (Build.FINGERPRINT.endsWith(":userdebug/dev-keys") || Build.FINGERPRINT.endsWith( + ":user/release-keys" + )) + && Build.PRODUCT.startsWith("sdk_gphone64_") + && Build.MODEL.startsWith("sdk_gphone64_")))) + // + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + //bluestacks + || "QC_Reference_Phone" == Build.BOARD && !"Xiaomi".equals( + Build.MANUFACTURER, + ignoreCase = true + ) + //bluestacks + || Build.MANUFACTURER.contains("Genymotion") + || Build.HOST.startsWith("Build") + //MSI App Player + || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic") + || Build.PRODUCT == "google_sdk") + // another Android SDK emulator check + /* || SystemProperties.getProp("ro.kernel.qemu") == "1") */ +} diff --git a/samples/testapp/src/commonMain/composeResources/values/strings.xml b/samples/testapp/src/commonMain/composeResources/values/strings.xml index 76c6f7dcf..1d57ae988 100644 --- a/samples/testapp/src/commonMain/composeResources/values/strings.xml +++ b/samples/testapp/src/commonMain/composeResources/values/strings.xml @@ -16,5 +16,6 @@ ISO mdoc Proximity Sharing ISO mdoc Proximity Reading ISO mdoc Multi-Device Testing + Test provisioning \ No newline at end of file diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/App.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/App.kt index 352b5855c..671c71f9d 100644 --- a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/App.kt +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/App.kt @@ -35,6 +35,7 @@ import com.android.identity.testapp.ui.IsoMdocMultiDeviceTestingScreen import com.android.identity.testapp.ui.IsoMdocProximityReadingScreen import com.android.identity.testapp.ui.IsoMdocProximitySharingScreen import com.android.identity.testapp.ui.PassphraseEntryFieldScreen +import com.android.identity.testapp.ui.ProvisioningTestScreen import com.android.identity.testapp.ui.QrCodesScreen import com.android.identity.testapp.ui.SecureEnclaveSecureAreaScreen import com.android.identity.testapp.ui.SoftwareSecureAreaScreen @@ -97,6 +98,7 @@ class App { onClickCloudSecureArea = { navController.navigate(CloudSecureAreaDestination.route) }, onClickSecureEnclaveSecureArea = { navController.navigate(SecureEnclaveSecureAreaDestination.route) }, onClickPassphraseEntryField = { navController.navigate(PassphraseEntryFieldDestination.route) }, + onClickIssuanceTestField = { navController.navigate(ProvisioningTestDestination.route) }, onClickConsentSheetList = { navController.navigate(ConsentModalBottomSheetListDestination.route) }, onClickQrCodes = { navController.navigate(QrCodesDestination.route) }, onClickIsoMdocProximitySharing = { navController.navigate(IsoMdocProximitySharingDestination.route) }, @@ -122,6 +124,9 @@ class App { composable(route = PassphraseEntryFieldDestination.route) { PassphraseEntryFieldScreen(showToast = { message -> showToast(message) }) } + composable(route = ProvisioningTestDestination.route) { + ProvisioningTestScreen() + } composable(route = ConsentModalBottomSheetListDestination.route) { ConsentModalBottomSheetListScreen( onConsentModalBottomSheetClicked = diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Destinations.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Destinations.kt index 782c6ac35..320a33685 100644 --- a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Destinations.kt +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Destinations.kt @@ -11,6 +11,7 @@ import identitycredential.samples.testapp.generated.resources.consent_modal_bott import identitycredential.samples.testapp.generated.resources.iso_mdoc_multi_device_testing_title import identitycredential.samples.testapp.generated.resources.iso_mdoc_proximity_reading_title import identitycredential.samples.testapp.generated.resources.iso_mdoc_proximity_sharing_title +import identitycredential.samples.testapp.generated.resources.provisioning_test_title import identitycredential.samples.testapp.generated.resources.passphrase_entry_field_screen_title import identitycredential.samples.testapp.generated.resources.qr_codes_screen_title import identitycredential.samples.testapp.generated.resources.secure_enclave_secure_area_screen_title @@ -58,6 +59,11 @@ data object PassphraseEntryFieldDestination : Destination { override val title = Res.string.passphrase_entry_field_screen_title } +data object ProvisioningTestDestination : Destination { + override val route: String = "provisioning_test" + override val title = Res.string.provisioning_test_title +} + data object ConsentModalBottomSheetListDestination : Destination { override val route = "consent_modal_bottom_sheet_list" override val title = Res.string.consent_modal_bottom_sheet_list_screen_title @@ -103,6 +109,7 @@ val appDestinations = listOf( SecureEnclaveSecureAreaDestination, CloudSecureAreaDestination, PassphraseEntryFieldDestination, + ProvisioningTestDestination, ConsentModalBottomSheetListDestination, ConsentModalBottomSheetDestination, QrCodesDestination, diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Platform.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Platform.kt index d6fb008bd..2a3e06601 100644 --- a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Platform.kt +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/Platform.kt @@ -1,5 +1,8 @@ package com.android.identity.testapp +import com.android.identity.securearea.CreateKeySettings +import com.android.identity.securearea.SecureArea + enum class Platform { ANDROID, IOS @@ -8,3 +11,9 @@ enum class Platform { expect val platform: Platform expect fun getLocalIpAddress(): String + +expect val platformIsEmulator: Boolean + +expect fun platformSecureArea(): SecureArea + +expect fun platformKeySetting(clientId: String): CreateKeySettings diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/provisioning/WalletHttpTransport.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/provisioning/WalletHttpTransport.kt new file mode 100644 index 000000000..9510e666b --- /dev/null +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/provisioning/WalletHttpTransport.kt @@ -0,0 +1,54 @@ +package com.android.identity.testapp.provisioning + +import com.android.identity.flow.transport.HttpTransport +import com.android.identity.util.Logger +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.HttpRequestTimeoutException +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.timeout +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.client.statement.readBytes +import kotlinx.coroutines.CancellationException +import kotlinx.io.bytestring.ByteString + +class WalletHttpTransport(private val baseUrl: String): HttpTransport { + companion object { + // The request timeout. + // + // TODO: make it possible to set the requestTimeout for each RPC call so + // this timeout can be specified in WalletServerProvider.kt where we do the + // waitUntilNotificationAvailable() call. + // + private const val REQUEST_TIMEOUT_SECONDS = 5*60 + } + + private val client = HttpClient(CIO) { + install(HttpTimeout) + } + + override suspend fun post( + url: String, + data: ByteString + ): ByteString { + val response = try { + client.post("$baseUrl/flow/$url") { + timeout { + requestTimeoutMillis = REQUEST_TIMEOUT_SECONDS.toLong()*1000 + } + setBody(data.toByteArray()) + } + } catch (e: HttpRequestTimeoutException) { + throw HttpTransport.TimeoutException("Timed out", e) + } catch (e: CancellationException) { + // important to propagate this one! + Logger.i("WalletHttpTransport", "Task cancelled", e) + throw e + } catch (e: Throwable) { + throw HttpTransport.ConnectionException("Error", e) + } + HttpTransport.processStatus(url, response.status.value, response.status.description) + return ByteString(response.readBytes()) + } +} \ No newline at end of file diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/provisioning/WalletServerProvider.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/provisioning/WalletServerProvider.kt new file mode 100644 index 000000000..5ec16f5c8 --- /dev/null +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/provisioning/WalletServerProvider.kt @@ -0,0 +1,209 @@ +package com.android.identity.testapp.provisioning + +import com.android.identity.cbor.Bstr +import com.android.identity.device.DeviceCheck +import com.android.identity.device.AssertionNonce +import com.android.identity.device.DeviceAttestation +import com.android.identity.flow.handler.FlowDispatcher +import com.android.identity.flow.handler.FlowDispatcherHttp +import com.android.identity.flow.handler.FlowExceptionMap +import com.android.identity.flow.handler.FlowNotifier +import com.android.identity.flow.handler.FlowNotifierPoll +import com.android.identity.flow.handler.FlowPollHttp +import com.android.identity.flow.transport.HttpTransport +import com.android.identity.issuance.ClientAuthentication +import com.android.identity.issuance.IssuingAuthority +import com.android.identity.issuance.IssuingAuthorityException +import com.android.identity.issuance.LandingUrlUnknownException +import com.android.identity.issuance.WalletApplicationCapabilities +import com.android.identity.issuance.WalletServer +import com.android.identity.issuance.WalletServerImpl +import com.android.identity.issuance.register +import com.android.identity.securearea.SecureArea +import com.android.identity.testapp.platformSecureArea +import com.android.identity.util.Logger +import io.ktor.utils.io.core.toByteArray +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.io.bytestring.ByteString +import kotlinx.io.bytestring.ByteStringBuilder +import kotlinx.io.bytestring.append +import kotlin.time.Duration.Companion.seconds + +/** + * An object used to connect to a remote wallet server. + */ +class WalletServerProvider( + private val baseUrl: String, + private val getWalletApplicationCapabilities: suspend () -> WalletApplicationCapabilities, +) { + private val secureArea: SecureArea = platformSecureArea() + private val instanceLock = Mutex() + private var instance: WalletServer? = null + private val issuingAuthorityMap = mutableMapOf() + + private var notificationsJob: Job? = null + private var clientId: String? = null + private var deviceAttestationId: String? = null + + companion object { + private const val TAG = "WalletServerProvider" + + private val RECONNECT_DELAY_INITIAL = 1.seconds + private val RECONNECT_DELAY_MAX = 30.seconds + + private val salt = byteArrayOf((0xe7).toByte(), 0x7c, (0xf8).toByte(), (0xec).toByte()) + + fun authenticationMessage(clientId: String, nonce: ByteString): ByteString { + val buffer = ByteStringBuilder() + buffer.append(salt) + buffer.append(clientId.toByteArray()) + buffer.append(nonce) + return buffer.toByteString() + } + } + + /** + * Connects to the remote wallet server. + * + * This process includes running through the authentication flow to prove to the remote wallet + * server that the client is in good standing, e.g. that Verified Boot is GREEN, Android patch + * level is sufficiently fresh, the app signature keys are as expected, and so on. + * + * This is usually only called for operations where the wallet actively needs to interact + * with the wallet server, e.g. when adding a new document or refreshing state. When the + * call succeeds, the resulting instance is cached and returned immediately in future calls. + * + * @return A [WalletServer] which can be used to interact with the remote wallet server. + * @throws HttpTransport.ConnectionException if unable to connect. + */ + suspend fun getWalletServer(): WalletServer { + instanceLock.withLock { + if (instance == null) { + Logger.i(TAG, "Creating new WalletServer instance: $baseUrl") + instance = getWalletServerUnlocked(baseUrl) + Logger.i(TAG, "Created new WalletServer instance: $baseUrl") + } else { + Logger.i(TAG, "Reusing existing WalletServer instance: $baseUrl") + } + return instance!! + } + } + + /** + * Connects to the remote wallet server, waiting for the server connection if needed. + */ + private suspend fun waitForWalletServer(): WalletServer { + var delay = RECONNECT_DELAY_INITIAL + while (true) { + try { + return getWalletServer() + } catch (err: HttpTransport.ConnectionException) { + delay(delay) + delay *= 2 + if (delay > RECONNECT_DELAY_MAX) { + delay = RECONNECT_DELAY_MAX + } + } + } + } + + /** + * Gets issuing authority by its id, caching instances. If unable to connect, suspend + * and wait until connecting is possible. + */ + suspend fun getIssuingAuthority(issuingAuthorityId: String): IssuingAuthority { + val instance = waitForWalletServer() + var delay = RECONNECT_DELAY_INITIAL + while (true) { + try { + instanceLock.withLock { + var issuingAuthority = issuingAuthorityMap[issuingAuthorityId] + if (issuingAuthority == null) { + issuingAuthority = instance.getIssuingAuthority(issuingAuthorityId) + issuingAuthorityMap[issuingAuthorityId] = issuingAuthority + } + return issuingAuthority + } + } catch (err: HttpTransport.ConnectionException) { + delay(delay) + delay *= 2 + if (delay > RECONNECT_DELAY_MAX) { + delay = RECONNECT_DELAY_MAX + } + } + } + } + + /** + * Creates an Issuing Authority by the [credentialIssuerUri] and [credentialConfigurationId], + * caching instances. If unable to connect, suspend and wait until connecting is possible. + */ + suspend fun createOpenid4VciIssuingAuthorityByUri( + credentialIssuerUri:String, + credentialConfigurationId: String + ): IssuingAuthority { + // Not allowed per spec, but double-check, so there are no surprises. + check(credentialIssuerUri.indexOf('#') < 0) + check(credentialConfigurationId.indexOf('#') < 0) + val id = "openid4vci#$credentialIssuerUri#$credentialConfigurationId" + return getIssuingAuthority(id) + } + + private suspend fun getWalletServerUnlocked(baseUrl: String): WalletServer { + val dispatcher: FlowDispatcher + val notifier: FlowNotifier + val exceptionMapBuilder = FlowExceptionMap.Builder() + IssuingAuthorityException.register(exceptionMapBuilder) + LandingUrlUnknownException.register(exceptionMapBuilder) + val httpClient = WalletHttpTransport(baseUrl) + val poll = FlowPollHttp(httpClient) + notifier = FlowNotifierPoll(poll) + notificationsJob = CoroutineScope(Dispatchers.Main).launch { + notifier.loop() + } + dispatcher = FlowDispatcherHttp(httpClient, exceptionMapBuilder.build()) + + // "root" is the entry point for the server, see FlowState annotation on + // com.android.identity.issuance.wallet.WalletServerState + val walletServer = WalletServerImpl( + flowPath = "root", + flowState = Bstr(byteArrayOf()), + flowDispatcher = dispatcher, + flowNotifier = notifier + ) + + val authentication = walletServer.authenticate() + val challenge = authentication.requestChallenge(clientId ?: "") + val deviceAttestation: DeviceAttestation? + if (clientId != challenge.clientId) { + // new client for this host + val result = DeviceCheck.generateAttestation(secureArea, challenge.clientId) + deviceAttestation = result.deviceAttestation + // TODO: save clientId and deviceAttestationId in storage + clientId = challenge.clientId + deviceAttestationId = result.deviceAttestationId + } else { + // existing client for this host + deviceAttestation = null + } + authentication.authenticate(ClientAuthentication( + deviceAttestation, + DeviceCheck.generateAssertion( + secureArea, + deviceAttestationId!!, + AssertionNonce(challenge.nonce) + ), + getWalletApplicationCapabilities() + )) + + authentication.complete() + + return walletServer + } +} \ No newline at end of file diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/ProvisioningTestScreen.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/ProvisioningTestScreen.kt new file mode 100644 index 000000000..c4f3a681b --- /dev/null +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/ProvisioningTestScreen.kt @@ -0,0 +1,53 @@ +package com.android.identity.testapp.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.identity.issuance.WalletApplicationCapabilities +import com.android.identity.testapp.platformIsEmulator +import com.android.identity.testapp.provisioning.WalletServerProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock + +@Composable +fun ProvisioningTestScreen() { + val serverAddress = remember { mutableStateOf("http://localhost:8080/server") } + LazyColumn( + modifier = Modifier.padding(8.dp) + ) { + item { + TextField(serverAddress.value, { serverAddress.value = it }, label = { + Text("Wallet Server address") + }) + } + item { + TextButton( + onClick = { + CoroutineScope(Dispatchers.Default).launch { + val provider = WalletServerProvider(serverAddress.value) { + WalletApplicationCapabilities( + generatedAt = Clock.System.now(), + androidKeystoreAttestKeyAvailable = true, + androidKeystoreStrongBoxAvailable = true, + androidIsEmulator = platformIsEmulator + ) + } + + val server = provider.getWalletServer() + println("Server: $server") + } + }, + content = { Text("Test provisioning") } + ) + } + } +} \ No newline at end of file diff --git a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/StartScreen.kt b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/StartScreen.kt index eef265e22..c4181ed1d 100644 --- a/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/StartScreen.kt +++ b/samples/testapp/src/commonMain/kotlin/com/android/identity/testapp/ui/StartScreen.kt @@ -21,6 +21,7 @@ import identitycredential.samples.testapp.generated.resources.consent_modal_bott import identitycredential.samples.testapp.generated.resources.iso_mdoc_multi_device_testing_title import identitycredential.samples.testapp.generated.resources.iso_mdoc_proximity_reading_title import identitycredential.samples.testapp.generated.resources.iso_mdoc_proximity_sharing_title +import identitycredential.samples.testapp.generated.resources.provisioning_test_title import identitycredential.samples.testapp.generated.resources.qr_codes_screen_title import identitycredential.samples.testapp.generated.resources.secure_enclave_secure_area_screen_title import identitycredential.samples.testapp.generated.resources.software_secure_area_screen_title @@ -34,6 +35,7 @@ fun StartScreen( onClickCloudSecureArea: () -> Unit = {}, onClickSecureEnclaveSecureArea: () -> Unit = {}, onClickPassphraseEntryField: () -> Unit = {}, + onClickIssuanceTestField: () -> Unit = {}, onClickConsentSheetList: () -> Unit = {}, onClickQrCodes: () -> Unit = {}, onClickIsoMdocProximitySharing: () -> Unit = {}, @@ -88,6 +90,12 @@ fun StartScreen( } } + item { + TextButton(onClick = onClickIssuanceTestField) { + Text(stringResource(Res.string.provisioning_test_title)) + } + } + item { TextButton(onClick = onClickConsentSheetList) { Text(stringResource(Res.string.consent_modal_bottom_sheet_list_screen_title)) diff --git a/samples/testapp/src/iosArm64Main/kotlin/com/android/identity/testapp/Platform.iosArm64.kt b/samples/testapp/src/iosArm64Main/kotlin/com/android/identity/testapp/Platform.iosArm64.kt new file mode 100644 index 000000000..d7d0a2acb --- /dev/null +++ b/samples/testapp/src/iosArm64Main/kotlin/com/android/identity/testapp/Platform.iosArm64.kt @@ -0,0 +1,3 @@ +package com.android.identity.testapp + +actual val platformIsEmulator: Boolean = false \ No newline at end of file diff --git a/samples/testapp/src/iosMain/kotlin/com/android/identity/testapp/PlatformIos.kt b/samples/testapp/src/iosMain/kotlin/com/android/identity/testapp/PlatformIos.kt index 2b155641f..6541707da 100644 --- a/samples/testapp/src/iosMain/kotlin/com/android/identity/testapp/PlatformIos.kt +++ b/samples/testapp/src/iosMain/kotlin/com/android/identity/testapp/PlatformIos.kt @@ -1,5 +1,9 @@ package com.android.identity.testapp +import com.android.identity.securearea.CreateKeySettings +import com.android.identity.securearea.SecureArea +import com.android.identity.securearea.SecureEnclaveSecureArea +import com.android.identity.storage.EphemeralStorageEngine import kotlinx.cinterop.ByteVar import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.NativePlacement @@ -63,3 +67,15 @@ actual fun getLocalIpAddress(): String { } throw IllegalStateException("Unable to determine local address") } + +private val secureEnclaveStorage = EphemeralStorageEngine() + +private val secureEnclaveSecureArea = SecureEnclaveSecureArea(secureEnclaveStorage) + +actual fun platformSecureArea(): SecureArea { + return secureEnclaveSecureArea +} + +actual fun platformKeySetting(clientId: String): CreateKeySettings { + return CreateKeySettings() +} diff --git a/samples/testapp/src/iosSimulatorArm64Main/kotlin/com/android/identity/testapp/Platform.iosSimulatorArm64.kt b/samples/testapp/src/iosSimulatorArm64Main/kotlin/com/android/identity/testapp/Platform.iosSimulatorArm64.kt new file mode 100644 index 000000000..32f6a3738 --- /dev/null +++ b/samples/testapp/src/iosSimulatorArm64Main/kotlin/com/android/identity/testapp/Platform.iosSimulatorArm64.kt @@ -0,0 +1,3 @@ +package com.android.identity.testapp + +actual val platformIsEmulator: Boolean = true \ No newline at end of file diff --git a/samples/testapp/src/iosX64Main/kotlin/com/android/identity/testapp/Platform.iosX64.kt b/samples/testapp/src/iosX64Main/kotlin/com/android/identity/testapp/Platform.iosX64.kt new file mode 100644 index 000000000..d7d0a2acb --- /dev/null +++ b/samples/testapp/src/iosX64Main/kotlin/com/android/identity/testapp/Platform.iosX64.kt @@ -0,0 +1,3 @@ +package com.android.identity.testapp + +actual val platformIsEmulator: Boolean = false \ No newline at end of file diff --git a/server-openid4vci/build.gradle.kts b/server-openid4vci/build.gradle.kts index 92c193e75..dcfcfbcf5 100644 --- a/server-openid4vci/build.gradle.kts +++ b/server-openid4vci/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":identity")) implementation(project(":identity-flow")) implementation(project(":processor-annotations")) + implementation(project(":identity-issuance-api")) implementation(project(":identity-issuance")) implementation(project(":identity-csa")) implementation(project(":identity-mdoc")) diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 52e6691b9..d95aa83f8 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(project(":identity")) implementation(project(":identity-flow")) implementation(project(":processor-annotations")) + implementation(project(":identity-issuance-api")) implementation(project(":identity-issuance")) implementation(project(":identity-csa")) implementation(project(":identity-mdoc")) diff --git a/settings.gradle.kts b/settings.gradle.kts index ce4d3aeec..426ba4f62 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -44,6 +44,7 @@ include(":identity-doctypes") include(":identity-android") include(":identity-android-legacy") include(":identity-issuance") +include(":identity-issuance-api") include(":identity-appsupport") include(":identity-csa") include(":identity-android-csa") diff --git a/wallet/build.gradle.kts b/wallet/build.gradle.kts index 32cfc2e02..afed2350e 100644 --- a/wallet/build.gradle.kts +++ b/wallet/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { implementation(project(":identity-sdjwt")) implementation(project(":identity-flow")) implementation(project(":identity-android")) + implementation(project(":identity-issuance-api")) implementation(project(":identity-issuance")) implementation(project(":identity-appsupport")) implementation(project(":mrtd-reader")) diff --git a/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt b/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt index 0f7c2691b..4aff6b76a 100644 --- a/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt +++ b/wallet/src/main/java/com/android/identity/issuance/remote/LocalDevelopmentEnvironment.kt @@ -5,6 +5,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.os.Handler import androidx.annotation.RawRes +import com.android.identity.device.DeviceAssertionMaker import com.android.identity.flow.server.Configuration import com.android.identity.flow.server.Resources import com.android.identity.flow.server.Storage @@ -31,6 +32,7 @@ import kotlin.reflect.cast internal class LocalDevelopmentEnvironment( context: Context, settingsModel: SettingsModel, + private val assertionMaker: DeviceAssertionMaker, private val secureArea: SecureArea, private val notifications: FlowNotifications, private val applicationSupportSupplier: WalletServerProvider.ApplicationSupportSupplier @@ -59,12 +61,13 @@ internal class LocalDevelopmentEnvironment( FlowNotifications::class -> notifications HttpClient::class -> httpClient SecureArea::class -> secureArea + DeviceAssertionMaker::class -> assertionMaker ApplicationSupport::class -> runBlocking { // We do not want to attempt to obtain applicationSupport ahead of time // as there may be connection problems and we want to deal with them only // if we have to, thus runBlocking is used. But this code is only used for // "dev:" Wallet Server. - applicationSupportSupplier.getApplicationSupport() + applicationSupportSupplier.getApplicationSupport().applicationSupport } else -> return null }) @@ -195,6 +198,8 @@ internal class LocalDevelopmentEnvironment( return when(name) { "ds_private_key.pem" -> getRawResourceAsString(R.raw.ds_private_key) "ds_certificate.pem" -> getRawResourceAsString(R.raw.ds_certificate) + "cloud_secure_area/certificate.pem" -> + getRawResourceAsString(R.raw.csa_certificate) "utopia_local/tos.html" -> context.resources.getString(R.string.utopia_local_issuing_authority_tos) "utopia_local_pid/tos.html" -> diff --git a/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerConnectionData.kt b/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerConnectionData.kt new file mode 100644 index 000000000..21308520b --- /dev/null +++ b/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerConnectionData.kt @@ -0,0 +1,11 @@ +package com.android.identity.issuance.remote + +import com.android.identity.cbor.annotation.CborSerializable + +@CborSerializable +data class WalletServerConnectionData( + val clientId: String, + val deviceAttestationId: String +) { + companion object +} \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt b/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt index 846f6116e..eee4e2052 100644 --- a/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt +++ b/wallet/src/main/java/com/android/identity/issuance/remote/WalletServerProvider.kt @@ -1,11 +1,11 @@ package com.android.identity.issuance.remote import android.content.Context -import com.android.identity.android.securearea.AndroidKeystoreCreateKeySettings import com.android.identity.android.securearea.AndroidKeystoreSecureArea import com.android.identity.cbor.Bstr import com.android.identity.cbor.DataItem -import com.android.identity.crypto.Algorithm +import com.android.identity.device.AssertionNonce +import com.android.identity.device.DeviceAssertionMaker import com.android.identity.flow.handler.FlowDispatcher import com.android.identity.flow.handler.FlowDispatcherHttp import com.android.identity.flow.handler.FlowDispatcherLocal @@ -18,15 +18,13 @@ import com.android.identity.flow.handler.SimpleCipher import com.android.identity.flow.transport.HttpTransport import com.android.identity.issuance.ApplicationSupport import com.android.identity.issuance.ClientAuthentication -import com.android.identity.issuance.ClientChallenge import com.android.identity.issuance.IssuingAuthority import com.android.identity.issuance.WalletApplicationCapabilities import com.android.identity.issuance.WalletServer import com.android.identity.issuance.WalletServerImpl -import com.android.identity.issuance.authenticationMessage -import com.android.identity.issuance.extractAttestationSequence import com.android.identity.issuance.wallet.WalletServerState -import com.android.identity.securearea.KeyInfo +import com.android.identity.device.DeviceCheck +import com.android.identity.device.DeviceAttestation import com.android.identity.util.Logger import com.android.identity_credential.wallet.SettingsModel import kotlinx.coroutines.CoroutineScope @@ -38,7 +36,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import org.bouncycastle.asn1.ASN1OctetString +import kotlinx.io.bytestring.ByteString import kotlin.time.Duration.Companion.seconds @@ -75,6 +73,17 @@ class WalletServerProvider( private var notificationsJob: Job? = null private var resetListeners = mutableListOf<()->Unit>() + private val storage = StorageImpl(context, "wallet_servers") + + val assertionMaker = DeviceAssertionMaker { assertion -> + val applicationSupportConnection = applicationSupportSupplier!!.getApplicationSupport() + DeviceCheck.generateAssertion( + secureArea = secureArea, + deviceAttestationId = applicationSupportConnection.deviceAttestationId, + assertion = assertion + ) + } + companion object { private const val TAG = "WalletServerProvider" @@ -156,9 +165,9 @@ class WalletServerProvider( instanceLock.withLock { if (instance == null) { Logger.i(TAG, "Creating new WalletServer instance: $baseUrl") - val server = getWalletServerUnlocked(baseUrl) - instance = server.first - applicationSupportSupplier = server.second + val connection = estableshWalletServerConnection(baseUrl) + instance = connection.server + applicationSupportSupplier = connection.applicationSupportSupplier Logger.i(TAG, "Created new WalletServer instance: $baseUrl") } else { Logger.i(TAG, "Reusing existing WalletServer instance: $baseUrl") @@ -228,17 +237,15 @@ class WalletServerProvider( } /** - * Gets ApplicationSupport object. It always comes from the server (either full wallet server - * or minimal wallet server). + * Gets ApplicationSupport object and data necessary to make use of it. + * It always comes from the server (either full wallet server or minimal wallet server). */ - suspend fun getApplicationSupport(): ApplicationSupport { + suspend fun getApplicationSupportConnection(): ApplicationSupportConnection { getWalletServer() return applicationSupportSupplier!!.getApplicationSupport() } - private suspend fun getWalletServerUnlocked( - baseUrl: String - ): Pair { + private suspend fun estableshWalletServerConnection(baseUrl: String): WalletServerConnection { val dispatcher: FlowDispatcher val notifier: FlowNotifier val exceptionMapBuilder = FlowExceptionMap.Builder() @@ -247,13 +254,14 @@ class WalletServerProvider( if (baseUrl == "dev:") { val builder = FlowDispatcherLocal.Builder() WalletServerState.registerAll(builder) - notifier = FlowNotificationsLocal(noopCipher) + notifier = FlowNotificationsLocal(CoroutineScope(Dispatchers.IO), noopCipher) applicationSupportSupplier = ApplicationSupportSupplier() { - val minServer = getWalletServerUnlocked(settingsModel.minServerUrl.value!!) - minServer.second.getApplicationSupport() + val minServer = estableshWalletServerConnection(settingsModel.minServerUrl.value!!) + minServer.applicationSupportSupplier.getApplicationSupport() } val environment = LocalDevelopmentEnvironment( - context, settingsModel, secureArea, notifier, applicationSupportSupplier) + context, settingsModel, assertionMaker, + secureArea, notifier, applicationSupportSupplier) dispatcher = WrapperFlowDispatcher(builder.build( environment, noopCipher, @@ -278,51 +286,67 @@ class WalletServerProvider( flowNotifier = notifier ) - val alias = "ClientKey:$baseUrl" - var keyInfo: KeyInfo? = null - var challenge: ClientChallenge? = null - val authentication = walletServer.authenticate() - try { - keyInfo = secureArea.getKeyInfo(alias) - } catch (ex: Exception) { - challenge = authentication.requestChallenge("") - } - if (keyInfo != null) { - val attestation = keyInfo.attestation - val seq = extractAttestationSequence(attestation.certChain!!) - val clientId = String(ASN1OctetString.getInstance(seq.getObjectAt(4)).octets) - challenge = authentication.requestChallenge(clientId) - if (clientId != challenge.clientId) { - secureArea.deleteKey(alias) - keyInfo = null - } + val connectionDataBytes = storage.get( + table = "Hosts", + peerId = "", + key = baseUrl + ) + var connectionData = if (connectionDataBytes == null) { + null + } else { + WalletServerConnectionData.fromCbor(connectionDataBytes.toByteArray()) } - val newClient = keyInfo == null - if (newClient) { - secureArea.createKey(alias, - AndroidKeystoreCreateKeySettings.Builder( - challenge!!.clientId.toByteArray() - ).build() + val authentication = walletServer.authenticate() + val challenge = authentication.requestChallenge(connectionData?.clientId ?: "") + val deviceAttestation: DeviceAttestation? + if (connectionData?.clientId != challenge.clientId) { + // new client + val result = DeviceCheck.generateAttestation(secureArea, challenge.clientId) + deviceAttestation = result.deviceAttestation + connectionData = WalletServerConnectionData( + clientId = challenge.clientId, + deviceAttestationId = result.deviceAttestationId ) - keyInfo = secureArea.getKeyInfo(alias) + if (connectionDataBytes == null) { + storage.insert( + table = "Hosts", + peerId = "", + data = ByteString(connectionData.toCbor()), + key = baseUrl + ) + } else { + storage.update( + table = "Hosts", + peerId = "", + data = ByteString(connectionData.toCbor()), + key = baseUrl + ) + } + } else { + deviceAttestation = null } - val message = authenticationMessage(challenge!!.clientId, challenge.nonce) authentication.authenticate(ClientAuthentication( - secureArea.sign(alias, Algorithm.ES256, message.toByteArray(), null), - if (newClient) { - keyInfo!!.attestation.certChain!! - } else null, + deviceAttestation, + DeviceCheck.generateAssertion( + secureArea, + connectionData.deviceAttestationId, + AssertionNonce(challenge.nonce) + ), getWalletApplicationCapabilities() )) authentication.complete() if (applicationSupportSupplier == null) { applicationSupportSupplier = ApplicationSupportSupplier { - walletServer.applicationSupport() + ApplicationSupportConnection( + applicationSupport = walletServer.applicationSupport(), + clientId = connectionData.clientId, + deviceAttestationId = connectionData.deviceAttestationId + ) } } - return Pair(walletServer, applicationSupportSupplier) + return WalletServerConnection(walletServer, applicationSupportSupplier) } /** @@ -355,18 +379,31 @@ class WalletServerProvider( } } - internal class ApplicationSupportSupplier(val factory: suspend () -> ApplicationSupport) { - private var applicationSupport: ApplicationSupport? = null + internal class ApplicationSupportSupplier( + val factory: suspend () -> ApplicationSupportConnection + ) { + private var connection: ApplicationSupportConnection? = null - suspend fun getApplicationSupport(): ApplicationSupport { - if (applicationSupport == null) { - applicationSupport = factory() + suspend fun getApplicationSupport(): ApplicationSupportConnection { + if (connection == null) { + connection = factory() } - return applicationSupport!! + return connection!! } suspend fun release() { - applicationSupport?.complete() + connection?.applicationSupport?.complete() } } + + class ApplicationSupportConnection( + val applicationSupport: ApplicationSupport, + val clientId: String, + val deviceAttestationId: String + ) + + internal class WalletServerConnection( + val server: WalletServer, + val applicationSupportSupplier: ApplicationSupportSupplier + ) } \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIcaoNfcTunnelDriver.kt b/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIcaoNfcTunnelDriver.kt deleted file mode 100644 index b6e461b90..000000000 --- a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIcaoNfcTunnelDriver.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.android.identity.issuance.simple - -import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnel -import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnelType -import com.android.identity.issuance.evidence.EvidenceResponse -import com.android.identity.issuance.evidence.EvidenceResponseIcaoNfcTunnel -import com.android.identity.mrtd.MrtdAccessData - -/** - * Drives the exchange with the chip in MRTD through NFC tunnel, sending commands to the chip - * and processing the responses. - */ -interface SimpleIcaoNfcTunnelDriver { - /** - * Initialize, supplying the desired data groups that should be read from the chip and, - * optionally, data to access the chip (must be provided when the tunnel was established using - * [EvidenceRequestIcaoNfcTunnelType.HANDSHAKE] with - * [EvidenceRequestIcaoNfcTunnel.passThrough] set to true). - */ - fun init(dataGroups: List, accessData: MrtdAccessData?) - - /** - * Handle the response from the chip and produce the next command. - * - * The first response in the sequence is always going to be the response to the handshake - * request (which serve to establish the tunnel and are not sent to/received from the chip). - * The rest of commands are sent to the chip and the responses come from the chip. - * - * When null is returned the tunnel is closed. - */ - suspend fun handleNfcTunnelResponse( - evidence: EvidenceResponseIcaoNfcTunnel - ): EvidenceRequestIcaoNfcTunnel? - - /** - * Collects all the data gathered by communicating through the tunnel as [EvidenceResponse] - * object. - * - * Called once the tunnel completes. - */ - fun collectEvidence(): EvidenceResponse -} \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt b/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt index faf67f3fa..bef648f1f 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/DocumentModel.kt @@ -20,6 +20,7 @@ import com.android.identity.credential.Credential import com.android.identity.credential.SecureAreaBoundCredential import com.android.identity.crypto.Algorithm import com.android.identity.crypto.EcCurve +import com.android.identity.device.AssertionBindingKeys import com.android.identity.mdoc.credential.MdocCredential import com.android.identity.document.Document import com.android.identity.document.DocumentStore @@ -38,6 +39,9 @@ import com.android.identity.issuance.DocumentExtensions.issuingAuthorityConfigur import com.android.identity.issuance.IssuingAuthority import com.android.identity.issuance.IssuingAuthorityException import com.android.identity.issuance.KeyPossessionProof +import com.android.identity.securearea.config.SecureAreaConfigurationAndroidKeystore +import com.android.identity.securearea.config.SecureAreaConfigurationCloud +import com.android.identity.securearea.config.SecureAreaConfigurationSoftware import com.android.identity.issuance.remote.WalletServerProvider import com.android.identity.mdoc.mso.MobileSecurityObjectParser import com.android.identity.mdoc.mso.StaticAuthDataParser @@ -842,28 +846,26 @@ class DocumentModel( val requestCredentialsFlow = issuer.requestCredentials(document.documentIdentifier) val credConfig = requestCredentialsFlow.getCredentialConfiguration(credentialFormat) - val secureArea = secureAreaRepository.getImplementation(credConfig.secureAreaIdentifier) - ?: throw IllegalArgumentException("No SecureArea ${credConfig.secureAreaIdentifier}") - val authKeySettings: CreateKeySettings = when (secureArea) { - is AndroidKeystoreSecureArea -> { - AndroidKeystoreCreateKeySettings.Builder(credConfig.challenge) - .applyConfiguration(Cbor.decode(credConfig.secureAreaConfiguration)) - .build() - } - - is SoftwareSecureArea -> { + val secureAreaConfiguration = credConfig.secureAreaConfiguration + val (secureArea, authKeySettings) = when (secureAreaConfiguration) { + is SecureAreaConfigurationSoftware -> Pair( + secureAreaRepository.getImplementation("SoftwareSecureArea"), SoftwareCreateKeySettings.Builder() - .applyConfiguration(Cbor.decode(credConfig.secureAreaConfiguration)) + .applyConfiguration(secureAreaConfiguration) .build() - } - - is CloudSecureArea -> { - CloudCreateKeySettings.Builder(credConfig.challenge) - .applyConfiguration(Cbor.decode(credConfig.secureAreaConfiguration)) + ) + is SecureAreaConfigurationAndroidKeystore -> Pair( + secureAreaRepository.getImplementation("AndroidKeystoreSecureArea"), + AndroidKeystoreCreateKeySettings.Builder(credConfig.challenge.toByteArray()) + .applyConfiguration(secureAreaConfiguration) .build() - } - - else -> throw IllegalStateException("Unexpected SecureArea $secureArea") + ) + is SecureAreaConfigurationCloud -> Pair( + secureAreaRepository.getImplementation(secureAreaConfiguration.cloudSecureAreaId), + CloudCreateKeySettings.Builder(credConfig.challenge.toByteArray()) + .applyConfiguration(secureAreaConfiguration) + .build() + ) } DocumentUtil.managedCredentialHelper( document, @@ -872,7 +874,7 @@ class DocumentModel( createCredential( credentialToReplace, credentialDomain, - secureArea, + secureArea!!, authKeySettings, ) }, @@ -890,7 +892,24 @@ class DocumentModel( ) ) } - val challenges = requestCredentialsFlow.sendCredentials(credentialRequests) + val applicationSupportConnection = + walletServerProvider.getApplicationSupportConnection() + val keysAssertion = walletServerProvider.assertionMaker.makeDeviceAssertion( + AssertionBindingKeys( + publicKeys = credentialRequests.map { request -> + request.secureAreaBoundKeyAttestation.publicKey + }, + nonce = credConfig.challenge, + clientId = applicationSupportConnection.clientId, + keyStorage = listOf(), + userAuthentication = listOf(), + issuedAt = Clock.System.now() + ) + ) + val challenges = requestCredentialsFlow.sendCredentials( + credentialRequests = credentialRequests, + keysAssertion = keysAssertion + ) if (challenges.isNotEmpty()) { val activity = this.activity!! if (challenges.size != document.pendingCredentials.size) { diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/NfcTunnelDriver.kt b/wallet/src/main/java/com/android/identity_credential/wallet/NfcTunnelDriver.kt deleted file mode 100644 index b62a1e69d..000000000 --- a/wallet/src/main/java/com/android/identity_credential/wallet/NfcTunnelDriver.kt +++ /dev/null @@ -1,301 +0,0 @@ -package com.android.identity_credential.wallet - -import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnel -import com.android.identity.issuance.evidence.EvidenceRequestIcaoNfcTunnelType -import com.android.identity.issuance.evidence.EvidenceResponse -import com.android.identity.issuance.evidence.EvidenceResponseIcaoNfcTunnel -import com.android.identity.issuance.evidence.EvidenceResponseIcaoNfcTunnelResult -import com.android.identity.issuance.simple.SimpleIcaoNfcTunnelDriver -import com.android.identity.mrtd.MrtdAccessData -import com.android.identity.mrtd.MrtdNfc -import com.android.identity.mrtd.MrtdNfcChipAccess -import com.android.identity.mrtd.MrtdNfcData -import com.android.identity.mrtd.MrtdNfcDataReader -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.runBlocking -import kotlinx.io.bytestring.ByteString -import net.sf.scuba.smartcards.CardService -import net.sf.scuba.smartcards.CommandAPDU -import net.sf.scuba.smartcards.ISO7816 -import net.sf.scuba.smartcards.ResponseAPDU -import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.jmrtd.PassportService -import org.jmrtd.lds.ChipAuthenticationInfo -import org.jmrtd.lds.ChipAuthenticationPublicKeyInfo -import org.jmrtd.lds.SODFile -import org.jmrtd.lds.SecurityInfo -import org.jmrtd.lds.icao.DG14File -import org.jmrtd.lds.icao.DG15File -import java.io.ByteArrayInputStream -import java.io.PrintWriter -import java.io.StringWriter -import java.lang.Exception -import java.lang.UnsupportedOperationException -import java.nio.ByteBuffer -import java.security.MessageDigest -import java.security.Signature -import java.security.interfaces.ECPublicKey - -/** - * Implements [SimpleIcaoNfcTunnelDriver] using JMRTD library. - * - * JMRTD is designed to be used in threading environment and blocking calls. Thus we create a - * dedicated thread and do command and response dispatching. - */ -class NfcTunnelDriver : SimpleIcaoNfcTunnelDriver { - private val requestChannel = Channel() - private val responseChannel = Channel() - private var dataGroups: List? = null - private var processingThread: Thread? = null - private var authenticating = true - private var progressPercent: Int = 0 - private var chipAuthenticationDone = false - private var activeAuthenticationDone = false - private var accessData: MrtdAccessData? = null - private var data: MrtdNfcData? = null - - override fun init(dataGroups: List, accessData: MrtdAccessData?) { - this.dataGroups = dataGroups - this.accessData = accessData - } - - override suspend fun handleNfcTunnelResponse( - evidence: EvidenceResponseIcaoNfcTunnel - ): EvidenceRequestIcaoNfcTunnel? { - if (processingThread == null) { - // Handshake response - val thread = Thread { - handleTunnel() - } - processingThread = thread - thread.name = "NFC Tunnel" - thread.start() - } else { - responseChannel.send(evidence) - } - return requestChannel.receive().request - } - - override fun collectEvidence(): EvidenceResponse { - val authenticationType = if (chipAuthenticationDone) - EvidenceResponseIcaoNfcTunnelResult.AdvancedAuthenticationType.CHIP - else if (activeAuthenticationDone) - EvidenceResponseIcaoNfcTunnelResult.AdvancedAuthenticationType.ACTIVE - else - EvidenceResponseIcaoNfcTunnelResult.AdvancedAuthenticationType.NONE - return EvidenceResponseIcaoNfcTunnelResult(authenticationType, data!!.dataGroups, data!!.sod) - } - - private fun handleChipAuthentication(service: PassportService): ByteString? { - val chipAuthenticationByteBudget = 56 - val dg14bytes: ByteString - try { - val input14 = service.getInputStream(PassportService.EF_DG14, PassportService.DEFAULT_MAX_BLOCKSIZE) - val bytesTotal = input14.length + chipAuthenticationByteBudget - dg14bytes = MrtdNfcDataReader.readStream(0, bytesTotal, input14) { progress -> - progressPercent = progress - } - } catch (err: Exception) { - return null - } - - val dg14 = DG14File(ByteArrayInputStream(dg14bytes.toByteArray())) - var capk: ChipAuthenticationPublicKeyInfo? = null - var ca: ChipAuthenticationInfo? = null - for (securityInfo in dg14.securityInfos) { - when (securityInfo) { - is ChipAuthenticationPublicKeyInfo -> capk = securityInfo - is ChipAuthenticationInfo -> ca = securityInfo - } - } - if (capk == null) { - throw IllegalStateException("ChipAuthenticationPublicKeyInfo not present") - } - try { - // Newer versions of JMRTD have method inferChipAuthenticationOIDfromPublicKeyOID - // which allows us to pass null is protocol oid - // Need this fix in some cases: https://sourceforge.net/p/jmrtd/bugs/76/ - val oid = if (ca != null) ca.objectIdentifier else SecurityInfo.ID_CA_ECDH_3DES_CBC_CBC - service.doEACCA(capk.keyId, oid, capk.objectIdentifier, capk.subjectPublicKey) - chipAuthenticationDone = true - } catch (err: Exception) { - val out = StringWriter() - err.printStackTrace(PrintWriter(out)) - throw err - } - - return dg14bytes - } - - private fun handleActiveAuthentication(service: PassportService): ByteString? { - val activeAuthenticationByteBudget = 56 - val dg15bytes: ByteString - try { - val input14 = service.getInputStream(PassportService.EF_DG15, PassportService.DEFAULT_MAX_BLOCKSIZE) - val bytesTotal = input14.length + activeAuthenticationByteBudget - dg15bytes = MrtdNfcDataReader.readStream(0, bytesTotal, input14) { progress -> - progressPercent = progress - } - } catch (err: Exception) { - return null - } - - val dg15 = DG15File(ByteArrayInputStream(dg15bytes.toByteArray())) - - val buffer = ByteBuffer.allocate(8) - buffer.putLong(System.currentTimeMillis()) - val publicKey = dg15.publicKey - if (publicKey !is ECPublicKey) { - return null - } - - val bits = publicKey.params.curve.field.fieldSize - val digest = if (bits >= 512) { - "SHA512" - } else if (bits >= 384) { - "SHA384" - } else if (bits >= 256) { - "SHA256" - } else { - "SHA224" - } - val res = service.doAA(publicKey, digest, "ECDSA", buffer.array()) - val dsa = Signature.getInstance(digest + "withPLAIN-ECDSA", BouncyCastleProvider.PROVIDER_NAME) - dsa.initVerify(dg15.publicKey) - dsa.update(buffer.array()) - try { - dsa.verify(res.response) - activeAuthenticationDone = true - } catch (err: Exception) { - throw err // fake passport - } - - return dg15bytes - } - - private fun handleTunnel() { - var service: PassportService? = null - val accessData = this.accessData - val rawService = TunnelCardService { - // if access data is null, mark transmission as encrypted, so that there - // is not encryption applied downstream. - accessData != null || service?.wrapper != null - } - - if (accessData == null) { - // Assume that the client have already performed basic access control step and - // a secure channel was established. - service = PassportService( - rawService, PassportService.NORMAL_MAX_TRANCEIVE_LENGTH, - PassportService.NORMAL_MAX_TRANCEIVE_LENGTH, - false, - false - ) - - // This is needed so that our PassportService is in the right state (as this command - // was issued on the client). We do not send actual commands to the tunnel. - service.sendSelectApplet(false) - - rawService.connect() - } else { - // Raw tunnel, perform BAC or PACE first. - rawService.connect() - - val chipAccess = MrtdNfcChipAccess(false) // TODO: enable mac? - service = chipAccess.open(rawService, accessData) { status -> - if (status is MrtdNfc.ReadingData) { - progressPercent = status.progressPercent - } - } - } - - val dg14bytes = handleChipAuthentication(service) - val dg15bytes = if (chipAuthenticationDone) null else handleActiveAuthentication(service) - - progressPercent = 0 - authenticating = false - - val data = MrtdNfcDataReader(dataGroups!!).read(rawService, service) { status -> - progressPercent = (status as MrtdNfc.ReadingData).progressPercent - } - - if (chipAuthenticationDone || activeAuthenticationDone) { - val sod = SODFile(ByteArrayInputStream(data.sod.toByteArray())) - val messageDigest = MessageDigest.getInstance(sod.digestAlgorithm) - if (dg14bytes != null) { - val digest = messageDigest.digest(dg14bytes.toByteArray()) - if (!digest.contentEquals(sod.dataGroupHashes[14])) { - chipAuthenticationDone = false - throw IllegalStateException("DG14 (Chip Authentication) stream cannot be validated") - } - } else if (dg15bytes != null) { - val digest = messageDigest.digest(dg15bytes.toByteArray()) - if (!digest.contentEquals(sod.dataGroupHashes[15])) { - activeAuthenticationDone = false - throw IllegalStateException("DG15 (Active Authentication) stream cannot be validated") - } - } else { - throw IllegalStateException("Should not happen") - } - } - - this.data = data - rawService.close() - } - - inner class TunnelCardService(private val passThrough: () -> Boolean) : CardService() { - private var closed = false - private var connected = false - - fun connect() { - connected = true - } - - override fun open() { - } - - override fun isOpen(): Boolean { - return !closed - } - - override fun transmit(commandAPDU: CommandAPDU?): ResponseAPDU { - if (!connected) { - // If not connected, just send success response. - val code = ISO7816.SW_NO_ERROR - return ResponseAPDU(byteArrayOf(((code.toInt()) ushr 8).toByte(), code.toByte())) - } - return runBlocking { - val requestType = - if (authenticating) { - EvidenceRequestIcaoNfcTunnelType.AUTHENTICATING - } else { - EvidenceRequestIcaoNfcTunnelType.READING - } - requestChannel.send(OptionalRequest( - EvidenceRequestIcaoNfcTunnel( - requestType, passThrough(), progressPercent, ByteString(commandAPDU!!.bytes)) - )) - val evidenceResponse = responseChannel.receive() - ResponseAPDU(evidenceResponse.response.toByteArray()) - } - } - - override fun getATR(): ByteArray { - throw UnsupportedOperationException() - } - - override fun close() { - closed = true - runBlocking { - requestChannel.send(OptionalRequest(null)) - } - } - - override fun isConnectionLost(e: Exception?): Boolean { - return false - } - } - - // Channel does not accept null, so use a wrapper - data class OptionalRequest(val request: EvidenceRequestIcaoNfcTunnel?) -} \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedEuPidIssuingAuthority.kt b/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedEuPidIssuingAuthority.kt deleted file mode 100644 index d99399f27..000000000 --- a/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedEuPidIssuingAuthority.kt +++ /dev/null @@ -1,313 +0,0 @@ -package com.android.identity_credential.wallet - -import android.content.Context -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborMap -import com.android.identity.cbor.toDataItemDateTimeString -import com.android.identity.cbor.toDataItemFullDate -import com.android.identity.crypto.EcCurve -import com.android.identity.document.NameSpacedData -import com.android.identity.documenttype.DocumentType -import com.android.identity.documenttype.knowntypes.EUPersonalID -import com.android.identity.issuance.CredentialConfiguration -import com.android.identity.issuance.DocumentConfiguration -import com.android.identity.issuance.RegistrationResponse -import com.android.identity.issuance.IssuingAuthorityConfiguration -import com.android.identity.issuance.MdocDocumentConfiguration -import com.android.identity.issuance.SdJwtVcDocumentConfiguration -import com.android.identity.issuance.evidence.EvidenceResponse -import com.android.identity.issuance.evidence.EvidenceResponseIcaoNfcTunnelResult -import com.android.identity.issuance.evidence.EvidenceResponseIcaoPassiveAuthentication -import com.android.identity.issuance.evidence.EvidenceResponseMessage -import com.android.identity.issuance.evidence.EvidenceResponseQuestionMultipleChoice -import com.android.identity.issuance.evidence.EvidenceResponseQuestionString -import com.android.identity.issuance.simple.SimpleIcaoNfcTunnelDriver -import com.android.identity.issuance.simple.SimpleIssuingAuthorityProofingGraph -import com.android.identity.securearea.KeyPurpose -import com.android.identity.storage.StorageEngine -import com.android.identity.mrtd.MrtdAccessData -import com.android.identity.mrtd.MrtdAccessDataCan -import com.android.identity.mrtd.MrtdNfcData -import com.android.identity.mrtd.MrtdNfcDataDecoder -import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn -import kotlinx.datetime.plus -import kotlinx.datetime.toLocalDateTime -import kotlin.time.Duration.Companion.days - -class SelfSignedEuPidIssuingAuthority( - application: WalletApplication, - storageEngine: StorageEngine, - emitOnStateChanged: suspend (documentId: String) -> Unit -) : SelfSignedIssuingAuthority( - application, - storageEngine, - emitOnStateChanged -) { - companion object { - private const val EUPID_NAMESPACE = EUPersonalID.EUPID_NAMESPACE - private const val EUPID_DOCTYPE = EUPersonalID.EUPID_DOCTYPE - - fun getConfiguration(context: Context): IssuingAuthorityConfiguration { - return IssuingAuthorityConfiguration( - identifier = "euPid_Utopia", - issuingAuthorityName = resourceString(context, R.string.utopia_eu_pid_issuing_authority_name), - issuingAuthorityLogo = pngData(context, R.drawable.utopia_pid_issuing_authority_logo), - issuingAuthorityDescription = resourceString(context, R.string.utopia_eu_pid_issuing_authority_description), - pendingDocumentInformation = DocumentConfiguration( - displayName = resourceString(context, R.string.utopia_eu_pid_issuing_authority_pending_document_title), - typeDisplayName = "Personal Identification Document", - cardArt = pngData(context, R.drawable.utopia_pid_card_art), - requireUserAuthenticationToViewDocument = false, - mdocConfiguration = null, - sdJwtVcDocumentConfiguration = null - ), - numberOfCredentialsToRequest = 3, - minCredentialValidityMillis = 30 * 24 * 3600L, - maxUsesPerCredentials = 1 - ) - } - } - - override suspend fun getConfiguration(): IssuingAuthorityConfiguration { - return getConfiguration(application.applicationContext) - } - - override val docType: String = EUPID_DOCTYPE - - private val tosAssets: Map = - mapOf("utopia_logo.png" to resourceBytes(R.drawable.utopia_pid_issuing_authority_logo)) - - override fun getProofingGraphRoot( - registrationResponse: RegistrationResponse - ): SimpleIssuingAuthorityProofingGraph.Node { - return SimpleIssuingAuthorityProofingGraph.create { - message( - "tos", - resourceString(R.string.utopia_eu_pid_issuing_authority_tos), - tosAssets, - resourceString(R.string.utopia_eu_pid_issuing_authority_accept), - resourceString(R.string.utopia_eu_pid_issuing_authority_reject), - ) - choice( - id = "path", - message = resourceString(R.string.utopia_eu_pid_issuing_authority_hardcoded_or_derived), - assets = mapOf(), - acceptButtonText = resourceString(R.string.utopia_eu_pid_issuing_authority_continue) - ) { - on(id = "hardcoded", text = resourceString(R.string.utopia_eu_pid_issuing_authority_hardcoded_option)) { - } - on(id = "passport", text = resourceString(R.string.utopia_eu_pid_issuing_authority_passport_option)) { - icaoPassiveAuthentication("passive", listOf(1)) - } - on(id = "id_card", text = resourceString(R.string.utopia_eu_pid_issuing_authority_id_option)) { - question("can", - resourceString(R.string.utopia_eu_pid_issuing_authority_enter_can), - mapOf(), "", - resourceString(R.string.utopia_eu_pid_issuing_authority_continue)) - icaoTunnel("tunnel", listOf(1), false) { - whenChipAuthenticated {} - whenActiveAuthenticated {} - whenNotAuthenticated {} - } - } - } - message( - "message", - resourceString(R.string.utopia_eu_pid_issuing_authority_application_finish), - mapOf(), - resourceString(R.string.utopia_eu_pid_issuing_authority_continue), - null - ) - requestNotificationPermission( - "notificationPermission", - permissionNotAvailableMessage = resourceString(R.string.permission_post_notifications_rationale_md), - grantPermissionButtonText = resourceString(R.string.permission_post_notifications_grant_permission_button_text), - continueWithoutPermissionButtonText = resourceString(R.string.permission_post_notifications_continue_without_permission_button_text), - assets = mapOf() - ) - } - } - - override fun createNfcTunnelHandler(): SimpleIcaoNfcTunnelDriver { - return NfcTunnelDriver() - } - - override fun getMrtdAccessData(collectedEvidence: Map): MrtdAccessData? { - return if (collectedEvidence.containsKey("can")) { - MrtdAccessDataCan((collectedEvidence["can"] as EvidenceResponseQuestionString).answer) - } else { - null - } - } - - override fun checkEvidence(collectedEvidence: Map): Boolean { - return (collectedEvidence["tos"] as EvidenceResponseMessage).acknowledged - } - - override fun generateDocumentConfiguration(collectedEvidence: Map): DocumentConfiguration { - return createDocumentConfiguration(collectedEvidence) - } - - override fun createCredentialConfiguration( - collectedEvidence: MutableMap - ): CredentialConfiguration { - val challenge = byteArrayOf(1, 2, 3) - return CredentialConfiguration( - challenge, - "AndroidKeystoreSecureArea", - Cbor.encode( - CborMap.builder() - .put("curve", EcCurve.P256.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN))) - .put("userAuthenticationRequired", true) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", 3 /* LSKF + Biometrics */) - .end().build() - ) - ) - } - - private fun createDocumentConfiguration(collectedEvidence: Map?): DocumentConfiguration { - val cardArt: ByteArray = pngData(application.applicationContext, R.drawable.utopia_pid_card_art) - - if (collectedEvidence == null) { - return DocumentConfiguration( - displayName = resourceString(R.string.utopia_eu_pid_issuing_authority_pending_document_title), - typeDisplayName = "Personal Identification Document", - cardArt = cardArt, - requireUserAuthenticationToViewDocument = false, - mdocConfiguration = null, - sdJwtVcDocumentConfiguration = null, - ) - } - - val staticData: NameSpacedData - - val now = Clock.System.now() - val issueDate = now - val expiryDate = now + 365.days * 5 - - val credType = application.documentTypeRepository.getDocumentTypeForMdoc(EUPID_DOCTYPE)!! - - val path = (collectedEvidence["path"] as EvidenceResponseQuestionMultipleChoice).answerId - if (path == "hardcoded") { - staticData = getSampleData(credType).build() - } else { - val icaoPassiveData = collectedEvidence["passive"] - val icaoTunnelData = collectedEvidence["tunnel"] - val mrtdData = if (icaoTunnelData is EvidenceResponseIcaoNfcTunnelResult) - MrtdNfcData(icaoTunnelData.dataGroups, icaoTunnelData.securityObject) - else if (icaoPassiveData is EvidenceResponseIcaoPassiveAuthentication) - MrtdNfcData(icaoPassiveData.dataGroups, icaoPassiveData.securityObject) - else - throw IllegalStateException("Should not happen") - val decoder = MrtdNfcDataDecoder() - val decoded = decoder.decode(mrtdData) - val firstName = decoded.firstName - val lastName = decoded.lastName - val sex = when (decoded.gender) { - "MALE" -> 1L - "FEMALE" -> 2L - else -> 0L - } - val timeZone = TimeZone.currentSystemDefault() - val dateOfBirth = LocalDate.parse(input = decoded.dateOfBirth, - format = LocalDate.Format { - // date of birth cannot be in future - yearTwoDigits(now.toLocalDateTime(timeZone).year - 99) - monthNumber() - dayOfMonth() - }) - val dateOfBirthInstant = dateOfBirth.atStartOfDayIn(timeZone) - // over 18/21 is calculated purely based on calendar date (not based on the birth time zone) - val ageOver18 = now > dateOfBirthInstant.plus(18, DateTimeUnit.YEAR, timeZone) - val ageOver21 = now > dateOfBirthInstant.plus(21, DateTimeUnit.YEAR, timeZone) - - // Make sure we set at least all the mandatory data elements - staticData = NameSpacedData.Builder() - .putEntryString(EUPID_NAMESPACE, "given_name", firstName) - .putEntryString(EUPID_NAMESPACE, "family_name", lastName) - .putEntry(EUPID_NAMESPACE, "birth_date", - Cbor.encode(dateOfBirth.toDataItemFullDate())) - .putEntryNumber(EUPID_NAMESPACE, "gender", sex) - .putEntry(EUPID_NAMESPACE, "issuance_date", - Cbor.encode(issueDate.toDataItemDateTimeString())) - .putEntry(EUPID_NAMESPACE, "expiry_date", - Cbor.encode(expiryDate.toDataItemDateTimeString()) - ) - .putEntryString(EUPID_NAMESPACE, "issuing_authority", - resourceString(R.string.utopia_eu_pid_issuing_authority_name)) - .putEntryString(EUPID_NAMESPACE, "issuing_country", - "ZZ") - .putEntryBoolean(EUPID_NAMESPACE, "age_over_18", ageOver18) - .putEntryBoolean(EUPID_NAMESPACE, "age_over_21", ageOver21) - .putEntryString(EUPID_NAMESPACE, "document_number", "1234567890") - .putEntryString(EUPID_NAMESPACE, "administrative_number", "123456789") - .build() - } - - val firstName = staticData.getDataElementString(EUPID_NAMESPACE, "given_name") - return DocumentConfiguration( - displayName = resourceString(R.string.utopia_eu_pid_issuing_authority_document_title, firstName), - typeDisplayName = "Personal Identification Document", - cardArt = cardArt, - requireUserAuthenticationToViewDocument = false, - mdocConfiguration = MdocDocumentConfiguration( - docType = EUPID_DOCTYPE, - staticData = staticData - ), - sdJwtVcDocumentConfiguration = SdJwtVcDocumentConfiguration( - vct = EUPersonalID.EUPID_VCT, - keyBound = true - ), - ) - } - - override fun developerModeRequestUpdate(currentConfiguration: DocumentConfiguration): DocumentConfiguration { - // The update consists of just slapping an extra 0 at the end of `administrative_number` - val newAdministrativeNumber = currentConfiguration.mdocConfiguration!!.staticData - .getDataElementString(EUPID_NAMESPACE, "administrative_number") + "0" - - val builder = NameSpacedData.Builder(currentConfiguration.mdocConfiguration!!.staticData) - builder.putEntryString( - EUPID_NAMESPACE, - "administrative_number", - newAdministrativeNumber - ) - - return DocumentConfiguration( - displayName = currentConfiguration.displayName, - typeDisplayName = "Personal Identification Document", - cardArt = currentConfiguration.cardArt, - requireUserAuthenticationToViewDocument = false, - mdocConfiguration = MdocDocumentConfiguration( - docType = EUPID_DOCTYPE, - staticData = builder.build() - ), - sdJwtVcDocumentConfiguration = SdJwtVcDocumentConfiguration( - vct = EUPersonalID.EUPID_VCT, - keyBound = true - ), - ) - } - - private fun getSampleData(documentType: DocumentType): NameSpacedData.Builder { - val builder = NameSpacedData.Builder() - for ((namespaceName, namespace) in documentType.mdocDocumentType!!.namespaces) { - for ((dataElementName, dataElement) in namespace.dataElements) { - if (dataElement.attribute.sampleValue != null) { - builder.putEntry( - namespaceName, - dataElementName, - Cbor.encode(dataElement.attribute.sampleValue!!) - ) - } - } - } - return builder - } -} \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedIssuingAuthority.kt b/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedIssuingAuthority.kt deleted file mode 100644 index f834052f7..000000000 --- a/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedIssuingAuthority.kt +++ /dev/null @@ -1,332 +0,0 @@ -package com.android.identity_credential.wallet - -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.RadialGradient -import android.graphics.Rect -import android.graphics.RectF -import android.graphics.Shader -import androidx.annotation.RawRes -import com.android.identity.cbor.Bstr -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborInt -import com.android.identity.cbor.DataItem -import com.android.identity.cbor.Tagged -import com.android.identity.cbor.Tstr -import com.android.identity.cbor.toDataItem -import com.android.identity.cose.Cose -import com.android.identity.cose.CoseLabel -import com.android.identity.cose.CoseNumberLabel -import com.android.identity.crypto.Algorithm -import com.android.identity.crypto.X509Cert -import com.android.identity.crypto.X509CertChain -import com.android.identity.crypto.EcPrivateKey -import com.android.identity.crypto.EcPublicKey -import com.android.identity.crypto.javaX509Certificate -import com.android.identity.documenttype.knowntypes.EUPersonalID -import com.android.identity.issuance.DocumentConfiguration -import com.android.identity.issuance.CredentialFormat -import com.android.identity.issuance.simple.SimpleIssuingAuthority -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.sdjwt.Issuer -import com.android.identity.sdjwt.SdJwtVcGenerator -import com.android.identity.sdjwt.util.JsonWebKey -import com.android.identity.storage.StorageEngine -import com.android.identity.util.Logger -import java.io.ByteArrayOutputStream -import java.nio.charset.StandardCharsets -import kotlin.math.ceil -import kotlin.random.Random -import kotlin.time.Duration.Companion.days -import kotlinx.datetime.Instant -import kotlinx.datetime.Clock -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put - - -abstract class SelfSignedIssuingAuthority( - val application: WalletApplication, - storageEngine: StorageEngine, - emitOnStateChanged: suspend (documentId: String) -> Unit -): SimpleIssuingAuthority( - storageEngine, - emitOnStateChanged -) { - companion object { - private const val TAG = "SelfSignedMdlIssuingAuthority" - - fun resourceString(context: Context, id: Int, vararg text: String): String { - return context.resources.getString(id, *text) - } - - fun resourceBytes(context: Context, id: Int): ByteArray { - val stream = context.resources.openRawResource(id) - val bytes = stream.readBytes() - stream.close() - return bytes - } - - fun jpegData(context: Context, resourceId: Int): ByteArray { - val baos = ByteArrayOutputStream() - BitmapFactory.decodeResource(context.resources, resourceId) - .compress(Bitmap.CompressFormat.JPEG, 90, baos) - return baos.toByteArray() - } - - fun pngData(context: Context, resourceId: Int): ByteArray { - val baos = ByteArrayOutputStream() - BitmapFactory.decodeResource(context.resources, resourceId) - .compress(Bitmap.CompressFormat.PNG, 100, baos) - return baos.toByteArray() - } - - } - - abstract val docType: String - - override fun createPresentationData(credentialFormat: CredentialFormat, - documentConfiguration: DocumentConfiguration, - deviceBoundKey: EcPublicKey - ): ByteArray { - when (credentialFormat) { - CredentialFormat.MDOC_MSO -> { - return createPresentationDataMdoc( - documentConfiguration, - deviceBoundKey - ) - } - CredentialFormat.SD_JWT_VC -> { - return createPresentationDataSdJwt( - documentConfiguration, - deviceBoundKey - ) - } - } - } - - private fun createPresentationDataSdJwt( - documentConfiguration: DocumentConfiguration, - deviceBoundKey: EcPublicKey - ): ByteArray { - - // For now, just use the mdoc data element names and only import tstr and numbers - // - val identityAttributes = buildJsonObject { - for (nsName in documentConfiguration.mdocConfiguration!!.staticData.nameSpaceNames) { - for (deName in documentConfiguration.mdocConfiguration!!.staticData.getDataElementNames(nsName)) { - val value = Cbor.decode( - documentConfiguration.mdocConfiguration!!.staticData.getDataElement(nsName, deName) - ) - when (value) { - is Tstr -> put(deName, value.asTstr) - is CborInt -> put(deName, value.asNumber.toString()) - else -> {} /* do nothing */ - } - } - } - } - - val sdJwtVcGenerator = SdJwtVcGenerator( - random = Random(42), - payload = identityAttributes, - docType = EUPersonalID.EUPID_VCT, - issuer = Issuer("https://example-issuer.com", Algorithm.ES256, "key-1") - ) - - val now = Clock.System.now() - - val timeSigned = now - val validFrom = now - val validUntil = validFrom + 30.days - - sdJwtVcGenerator.publicKey = JsonWebKey(deviceBoundKey) - sdJwtVcGenerator.timeSigned = timeSigned - sdJwtVcGenerator.timeValidityBegin = validFrom - sdJwtVcGenerator.timeValidityEnd = validUntil - - // Just use the mdoc Document Signing key for now - // - ensureDocumentSigningKey() - val sdJwt = sdJwtVcGenerator.generateSdJwt(documentSigningKey) - - return sdJwt.toString().toByteArray() - } - - private fun createPresentationDataMdoc( - documentConfiguration: DocumentConfiguration, - deviceBoundKey: EcPublicKey - ): ByteArray { - val now = Clock.System.now() - - // Create AuthKeys and MSOs, make sure they're valid for a long time - val timeSigned = now - val validFrom = now - val validUntil = Instant.fromEpochMilliseconds(validFrom.toEpochMilliseconds() + 365*24*3600*1000L) - - // Generate an MSO and issuer-signed data for this authentication key. - val msoGenerator = MobileSecurityObjectGenerator( - "SHA-256", - docType, - deviceBoundKey - ) - msoGenerator.setValidityInfo(timeSigned, validFrom, validUntil, null) - val randomProvider = Random.Default - val issuerNameSpaces = MdocUtil.generateIssuerNameSpaces( - documentConfiguration.mdocConfiguration!!.staticData, - randomProvider, - 16, - null - ) - for (nameSpaceName in issuerNameSpaces.keys) { - val digests = MdocUtil.calculateDigestsForNameSpace( - nameSpaceName, - issuerNameSpaces, - Algorithm.SHA256 - ) - msoGenerator.addDigestIdsForNamespace(nameSpaceName, digests) - } - - ensureDocumentSigningKey() - val mso = msoGenerator.generate() - val taggedEncodedMso = Cbor.encode(Tagged(Tagged.ENCODED_CBOR, Bstr(mso))) - val protectedHeaders = mapOf(Pair( - CoseNumberLabel(Cose.COSE_LABEL_ALG), - Algorithm.ES256.coseAlgorithmIdentifier.toDataItem() - )) - val unprotectedHeaders = mapOf(Pair( - CoseNumberLabel(Cose.COSE_LABEL_X5CHAIN), - X509CertChain(listOf( - X509Cert(documentSigningKeyCert.encodedCertificate)) - ).toDataItem() - )) - val encodedIssuerAuth = Cbor.encode( - Cose.coseSign1Sign( - documentSigningKey, - taggedEncodedMso, - true, - Algorithm.ES256, - protectedHeaders, - unprotectedHeaders - ).toDataItem() - ) - - val issuerProvidedAuthenticationData = StaticAuthDataGenerator( - issuerNameSpaces, - encodedIssuerAuth - ).generate() - - Logger.d(TAG, "Created MSO") - return issuerProvidedAuthenticationData - } - - private lateinit var documentSigningKey: EcPrivateKey - private lateinit var documentSigningKeyCert: X509Cert - - private fun getRawResourceAsString(@RawRes resourceId: Int): String { - val inputStream = application.applicationContext.resources.openRawResource(resourceId) - val bytes = inputStream.readBytes() - return String(bytes, StandardCharsets.UTF_8) - } - - private fun ensureDocumentSigningKey() { - if (this::documentSigningKey.isInitialized) { - return - } - - // The IACA and DS certificates and keys can be regenerated using the following steps - // - // $ ./gradlew --quiet runIdentityCtl --args generateIaca - // Generated IACA certificate and private key. - // - Wrote private key to iaca_private_key.pem - // - Wrote IACA certificate to iaca_certificate.pem - // - // $ ./gradlew --quiet runIdentityCtl --args generateDs - // Generated DS certificate and private key. - // - Wrote private key to ds_private_key.pem - // - Wrote DS certificate to ds_certificate.pem - // - - documentSigningKeyCert = X509Cert.fromPem( - getRawResourceAsString(R.raw.ds_certificate) - ) - Logger.d(TAG, "Cert: " + documentSigningKeyCert.javaX509Certificate.toString()) - documentSigningKey = EcPrivateKey.fromPem( - getRawResourceAsString(R.raw.ds_private_key), - documentSigningKeyCert.ecPublicKey - ) - - } - - protected fun bitmapData(resourceId: Int): ByteArray { - val baos = ByteArrayOutputStream() - BitmapFactory.decodeResource( - application.applicationContext.resources, - resourceId - ).compress(Bitmap.CompressFormat.JPEG, 90, baos) - return baos.toByteArray() - } - - protected fun createArtwork(color1: Int, - color2: Int, - portrait: ByteArray?, - artworkText: String): ByteArray { - val width = 800 - val height = ceil(width.toFloat() * 2.125 / 3.375).toInt() - val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - val bgPaint = Paint() - bgPaint.setShader( - RadialGradient( - width / 2f, height / 2f, - height / 0.5f, color1, color2, Shader.TileMode.MIRROR - ) - ) - val round = bitmap.width / 25f - canvas.drawRoundRect( - 0f, - 0f, - bitmap.width.toFloat(), - bitmap.height.toFloat(), - round, - round, - bgPaint - ) - - if (portrait != null) { - val portraitBitmap = BitmapFactory.decodeByteArray(portrait, 0, portrait.size) - val src = Rect(0, 0, portraitBitmap.width, portraitBitmap.height) - val scale = height * 0.7f / portraitBitmap.height.toFloat() - val dst = RectF(round, round, portraitBitmap.width*scale, portraitBitmap.height*scale); - canvas.drawBitmap(portraitBitmap, src, dst, bgPaint) - } - - val paint = Paint(Paint.ANTI_ALIAS_FLAG) - paint.setColor(Color.WHITE) - paint.textSize = bitmap.width / 10.0f - paint.setShadowLayer(2.0f, 1.0f, 1.0f, Color.BLACK) - val bounds = Rect() - paint.getTextBounds(artworkText, 0, artworkText.length, bounds) - val textPadding = bitmap.width/25f - val x: Float = textPadding - val y: Float = bitmap.height - bounds.height() - textPadding + paint.textSize/2 - canvas.drawText(artworkText, x, y, paint) - - val baos = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos) - return baos.toByteArray() - } - - protected fun resourceString(id: Int, vararg text: String): String { - return Companion.resourceString(application.applicationContext, id, *text) - } - - protected fun resourceBytes(id: Int): ByteArray { - return Companion.resourceBytes(application.applicationContext, id) - } -} diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedMdlIssuingAuthority.kt b/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedMdlIssuingAuthority.kt deleted file mode 100644 index 2a676ebf4..000000000 --- a/wallet/src/main/java/com/android/identity_credential/wallet/SelfSignedMdlIssuingAuthority.kt +++ /dev/null @@ -1,545 +0,0 @@ -package com.android.identity_credential.wallet - -import android.content.Context -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborArray -import com.android.identity.cbor.CborMap -import com.android.identity.cbor.toDataItemDateTimeString -import com.android.identity.cbor.toDataItemFullDate -import com.android.identity.crypto.EcCurve -import com.android.identity.document.NameSpacedData -import com.android.identity.documenttype.DocumentType -import com.android.identity.documenttype.knowntypes.DrivingLicense -import com.android.identity.issuance.CredentialConfiguration -import com.android.identity.issuance.DocumentConfiguration -import com.android.identity.issuance.RegistrationResponse -import com.android.identity.issuance.IssuingAuthorityConfiguration -import com.android.identity.issuance.MdocDocumentConfiguration -import com.android.identity.issuance.evidence.EvidenceResponse -import com.android.identity.issuance.evidence.EvidenceResponseCreatePassphrase -import com.android.identity.issuance.evidence.EvidenceResponseIcaoNfcTunnelResult -import com.android.identity.issuance.evidence.EvidenceResponseIcaoPassiveAuthentication -import com.android.identity.issuance.evidence.EvidenceResponseMessage -import com.android.identity.issuance.evidence.EvidenceResponseQuestionMultipleChoice -import com.android.identity.issuance.simple.SimpleIcaoNfcTunnelDriver -import com.android.identity.issuance.simple.SimpleIssuingAuthorityProofingGraph -import com.android.identity.securearea.KeyPurpose -import com.android.identity.securearea.PassphraseConstraints -import com.android.identity.securearea.toDataItem -import com.android.identity.storage.StorageEngine -import com.android.identity.mrtd.MrtdAccessData -import com.android.identity.mrtd.MrtdNfcData -import com.android.identity.mrtd.MrtdNfcDataDecoder -import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.LocalDate -import kotlinx.datetime.TimeZone -import kotlinx.datetime.atStartOfDayIn -import kotlinx.datetime.plus -import kotlinx.datetime.toLocalDateTime -import kotlin.time.Duration.Companion.days - -class SelfSignedMdlIssuingAuthority( - application: WalletApplication, - storageEngine: StorageEngine, - emitOnStateChanged: suspend (documentId: String) -> Unit -) : SelfSignedIssuingAuthority(application, storageEngine, emitOnStateChanged) { - - companion object { - private const val MDL_DOCTYPE = DrivingLicense.MDL_DOCTYPE - private const val MDL_NAMESPACE = DrivingLicense.MDL_NAMESPACE - private const val AAMVA_NAMESPACE = DrivingLicense.AAMVA_NAMESPACE - - fun getConfiguration(context: Context): IssuingAuthorityConfiguration { - return IssuingAuthorityConfiguration( - identifier = "mDL_Utopia", - issuingAuthorityName = resourceString(context, R.string.utopia_mdl_issuing_authority_name), - issuingAuthorityLogo = pngData(context, R.drawable.utopia_dmv_issuing_authority_logo), - issuingAuthorityDescription = resourceString(context, R.string.utopia_mdl_issuing_authority_description), - pendingDocumentInformation = DocumentConfiguration( - displayName = resourceString(context, R.string.utopia_mdl_issuing_authority_pending_document_title), - typeDisplayName = "Personal Identification Document", - cardArt = pngData(context, R.drawable.utopia_driving_license_card_art), - requireUserAuthenticationToViewDocument = false, - mdocConfiguration = null, - sdJwtVcDocumentConfiguration = null - ), - numberOfCredentialsToRequest = 3, - minCredentialValidityMillis = 30 * 24 * 3600L, - maxUsesPerCredentials = 1 - ) - } - } - - override suspend fun getConfiguration(): IssuingAuthorityConfiguration { - return getConfiguration(application.applicationContext) - } - - override val docType: String = MDL_DOCTYPE - private val tosAssets: Map = - mapOf("utopia_logo.png" to resourceBytes(R.drawable.utopia_dmv_issuing_authority_logo)) - - override fun getProofingGraphRoot( - registrationResponse: RegistrationResponse, - ): SimpleIssuingAuthorityProofingGraph.Node { - val devAssets = mapOf("experiment_icon.svg" to resourceBytes(R.raw.experiment_icon)) - val devNotice = "\n\n![Development Setting](experiment_icon.svg){style=height:1.5em;vertical-align:middle;margin-right:0.5em}" + - " Development Mode setting" - return SimpleIssuingAuthorityProofingGraph.create { - message( - "tos", - resourceString(R.string.utopia_mdl_issuing_authority_tos), - tosAssets, - resourceString(R.string.utopia_mdl_issuing_authority_accept), - resourceString(R.string.utopia_mdl_issuing_authority_reject), - ) - choice( - id = "path", - message = resourceString(R.string.utopia_mdl_issuing_authority_hardcoded_or_derived), - assets = mapOf(), - acceptButtonText = "Continue" - ) { - on(id = "hardcoded", text = resourceString(R.string.utopia_mdl_issuing_authority_hardcoded_option)) { - if (registrationResponse.developerModeEnabled) { - choice( - id = "devmode_image_format", - message = "Choose format for images in mDL $devNotice", - assets = devAssets, - acceptButtonText = "Continue" - ) { - on(id = "devmode_image_format_jpeg", text = "JPEG") {} - on(id = "devmode_image_format_jpeg2000", text = "JPEG 2000") {} - } - } - } - on(id = "passport", text = resourceString(R.string.utopia_mdl_issuing_authority_passport_option)) { - icaoTunnel("tunnel", listOf(1, 2, 7), true) { - whenChipAuthenticated {} - whenActiveAuthenticated {} - whenNotAuthenticated {} - } - } - } - if (registrationResponse.developerModeEnabled) { - choice( - id = "devmode_sa", - message = "Choose Secure Area $devNotice", - assets = devAssets, - acceptButtonText = "Continue" - ) { - on(id = "devmode_sa_android", text = "Android Keystore") { - choice( - id = "devmode_sa_android_use_strongbox", - message = "Use StrongBox $devNotice", - assets = devAssets, - acceptButtonText = "Continue" - ) { - on(id = "devmode_sa_android_use_strongbox_no", text = "Don't use StrongBox") {} - on(id = "devmode_sa_android_use_strongbox_yes", text = "Use StrongBox") {} - } - choice( - id = "devmode_sa_android_user_auth", - message = "Choose user authentication $devNotice", - assets = devAssets, - acceptButtonText = "Continue" - ) { - on(id = "devmode_sa_android_user_auth_lskf_biometrics", text = "LSKF or Biometrics") {} - on(id = "devmode_sa_android_user_auth_lskf", text = "Only LSKF") {} - on(id = "devmode_sa_android_user_auth_biometrics", text = "Only Biometrics") {} - on(id = "devmode_sa_android_user_auth_none", text = "None") {} - } - choice( - id = "devmode_sa_android_mdoc_auth", - message = "Choose mdoc authentication mode and EC curve $devNotice", - assets = devAssets, - acceptButtonText = "Continue" - ) { - on(id = "devmode_sa_android_mdoc_auth_ecdsa_p256", text = "ECDSA w/ P-256") {} - on(id = "devmode_sa_android_mdoc_auth_ed25519", text = "EdDSA w/ Ed25519") {} - on(id = "devmode_sa_android_mdoc_auth_ed448", text = "EdDSA w/ Ed448") {} - on(id = "devmode_sa_android_mdoc_auth_ecdh_p256", text = "ECDH w/ P-256") {} - on(id = "devmode_sa_android_mdoc_auth_x25519", text = "XDH w/ X25519") {} - on(id = "devmode_sa_android_mdoc_auth_x448", text = "XDH w/ X448") {} - } - } - - on(id = "devmode_sa_software", text = "Software") { - choice( - id = "devmode_sa_software_passphrase_complexity", - message = "Choose what kind of passphrase to use $devNotice", - assets = devAssets, - acceptButtonText = "Continue" - ) { - on(id = "devmode_sa_software_passphrase_6_digit_pin", text = "6-digit PIN") { - createPassphrase( - "devmode_sa_software_passphrase", - message = "## Choose 6-digit PIN\n\nChoose the PIN to use for the document.\n\nThis is asked every time the document is presented so make sure you memorize it and don't share it with anyone else. $devNotice", - verifyMessage = "## Verify PIN\n\nEnter the PIN you chose in the previous screen. $devNotice", - assets = devAssets, - PassphraseConstraints.PIN_SIX_DIGITS - ) - } - on(id = "devmode_sa_software_passphrase_8_char_or_longer_passphrase", text = "Passphrase 8 chars or longer") { - createPassphrase( - "devmode_sa_software_passphrase", - message = "## Choose passphrase\n\nChoose the passphrase to use for the document.\n\nThis is asked every time the document is presented so make sure you memorize it and don't share it with anyone else. $devNotice", - verifyMessage = "## Verify passphrase\n\nEnter the passphrase you chose in the previous screen. $devNotice", - assets = devAssets, - PassphraseConstraints(8, Int.MAX_VALUE, false) - ) - } - on(id = "devmode_sa_software_passphrase_none", text = "None") {} - } - choice( - id = "devmode_sa_software_mdoc_auth", - message = "Choose mdoc authentication mode and EC curve $devNotice", - assets = devAssets, - acceptButtonText = "Continue" - ) { - on(id = "devmode_sa_software_mdoc_auth_ecdsa_p256", text = "ECDSA w/ P-256") {} - on(id = "devmode_sa_software_mdoc_auth_ecdsa_p384", text = "ECDSA w/ P-384") {} - on(id = "devmode_sa_software_mdoc_auth_ecdsa_p521", text = "ECDSA w/ P-521") {} - on(id = "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp256r1", text = "ECDSA w/ brainpoolP256r1") {} - on(id = "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp320r1", text = "ECDSA w/ brainpoolP320r1") {} - on(id = "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp384r1", text = "ECDSA w/ brainpoolP384r1") {} - on(id = "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp512r1", text = "ECDSA w/ brainpoolP512r1") {} - on(id = "devmode_sa_software_mdoc_auth_ed25519", text = "EdDSA w/ Ed25519") {} - on(id = "devmode_sa_software_mdoc_auth_ed448", text = "EdDSA w/ Ed448") {} - on(id = "devmode_sa_software_mdoc_auth_ecdh_p256", text = "ECDH w/ P-256") {} - on(id = "devmode_sa_software_mdoc_auth_ecdh_p384", text = "ECDH w/ P-384") {} - on(id = "devmode_sa_software_mdoc_auth_ecdh_p521", text = "ECDH w/ P-521") {} - on(id = "devmode_sa_software_mdoc_auth_ecdh_brainpoolp256r1", text = "ECDH w/ brainpoolP256r1") {} - on(id = "devmode_sa_software_mdoc_auth_ecdh_brainpoolp320r1", text = "ECDH w/ brainpoolP320r1") {} - on(id = "devmode_sa_software_mdoc_auth_ecdh_brainpoolp384r1", text = "ECDH w/ brainpoolP384r1") {} - on(id = "devmode_sa_software_mdoc_auth_ecdh_brainpoolp512r1", text = "ECDH w/ brainpoolP512r1") {} - on(id = "devmode_sa_software_mdoc_auth_x25519", text = "XDH w/ X25519") {} - on(id = "devmode_sa_software_mdoc_auth_x448", text = "XDH w/ X448") {} - } - } - } - } - message( - "message", - message = resourceString(R.string.utopia_mdl_issuing_authority_application_finish), - assets = mapOf(), - acceptButtonText = resourceString(R.string.utopia_mdl_issuing_authority_continue), - null - ) - requestNotificationPermission( - "notificationPermission", - permissionNotAvailableMessage = resourceString(R.string.permission_post_notifications_rationale_md), - grantPermissionButtonText = resourceString(R.string.permission_post_notifications_grant_permission_button_text), - continueWithoutPermissionButtonText = resourceString(R.string.permission_post_notifications_continue_without_permission_button_text), - assets = mapOf() - ) - } - } - - override fun getMrtdAccessData(collectedEvidence: Map): MrtdAccessData? { - return null - } - - override fun createNfcTunnelHandler(): SimpleIcaoNfcTunnelDriver { - return NfcTunnelDriver() - } - - override fun checkEvidence(collectedEvidence: Map): Boolean { - return (collectedEvidence["tos"] as EvidenceResponseMessage).acknowledged - } - - override fun generateDocumentConfiguration(collectedEvidence: Map): DocumentConfiguration { - return createDocumentConfiguration(collectedEvidence) - } - - override fun createCredentialConfiguration( - collectedEvidence: MutableMap - ): CredentialConfiguration { - val challenge = byteArrayOf(1, 2, 3) - if (!collectedEvidence.containsKey("devmode_sa")) { - return CredentialConfiguration( - challenge, - "AndroidKeystoreSecureArea", - Cbor.encode( - CborMap.builder() - .put("curve", EcCurve.P256.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN))) - .put("userAuthenticationRequired", true) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", 3 /* LSKF + Biometrics */) - .end().build() - ) - ) - } - - val chosenSa = (collectedEvidence["devmode_sa"] as EvidenceResponseQuestionMultipleChoice).answerId - when (chosenSa) { - "devmode_sa_android" -> { - val useStrongBox = when ((collectedEvidence["devmode_sa_android_use_strongbox"] - as EvidenceResponseQuestionMultipleChoice).answerId) { - "devmode_sa_android_use_strongbox_yes" -> true - "devmode_sa_android_use_strongbox_no" -> false - else -> throw IllegalStateException() - } - val userAuthType = when ((collectedEvidence["devmode_sa_android_user_auth"] - as EvidenceResponseQuestionMultipleChoice).answerId) { - "devmode_sa_android_user_auth_lskf_biometrics" -> 3 - "devmode_sa_android_user_auth_lskf" -> 1 - "devmode_sa_android_user_auth_biometrics" -> 2 - "devmode_sa_android_user_auth_none" -> 0 - else -> throw IllegalStateException() - } - val (curve, purposes) = when ((collectedEvidence["devmode_sa_android_mdoc_auth"] - as EvidenceResponseQuestionMultipleChoice).answerId) { - "devmode_sa_android_mdoc_auth_ecdsa_p256" -> Pair(EcCurve.P256, setOf(KeyPurpose.SIGN)) - "devmode_sa_android_mdoc_auth_ed25519" -> Pair(EcCurve.ED25519, setOf(KeyPurpose.SIGN)) - "devmode_sa_android_mdoc_auth_ed448" -> Pair(EcCurve.ED448, setOf(KeyPurpose.SIGN)) - "devmode_sa_android_mdoc_auth_ecdh_p256" -> Pair(EcCurve.P256, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_android_mdoc_auth_x25519" -> Pair(EcCurve.X25519, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_android_mdoc_auth_x448" -> Pair(EcCurve.X448, setOf(KeyPurpose.AGREE_KEY)) - else -> throw IllegalStateException() - } - return CredentialConfiguration( - challenge, - "AndroidKeystoreSecureArea", - Cbor.encode( - CborMap.builder() - .put("useStrongBox", useStrongBox) - .put("userAuthenticationRequired", (userAuthType != 0)) - .put("userAuthenticationTimeoutMillis", 0L) - .put("userAuthenticationTypes", userAuthType) - .put("curve", curve.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(purposes)) - .end().build() - ) - ) - - } - - "devmode_sa_software" -> { - val (curve, purposes) = when ((collectedEvidence["devmode_sa_software_mdoc_auth"] - as EvidenceResponseQuestionMultipleChoice).answerId) { - "devmode_sa_software_mdoc_auth_ecdsa_p256" -> Pair(EcCurve.P256, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ecdsa_p384" -> Pair(EcCurve.P384, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ecdsa_p521" -> Pair(EcCurve.P521, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp256r1" -> Pair(EcCurve.BRAINPOOLP256R1, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp320r1" -> Pair(EcCurve.BRAINPOOLP320R1, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp384r1" -> Pair(EcCurve.BRAINPOOLP384R1, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ecdsa_brainpoolp512r1" -> Pair(EcCurve.BRAINPOOLP512R1, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ed25519" -> Pair(EcCurve.ED25519, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ed448" -> Pair(EcCurve.ED448, setOf(KeyPurpose.SIGN)) - "devmode_sa_software_mdoc_auth_ecdh_p256" -> Pair(EcCurve.P256, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_ecdh_p384" -> Pair(EcCurve.P384, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_ecdh_p521" -> Pair(EcCurve.P521, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_ecdh_brainpoolp256r1" -> Pair(EcCurve.BRAINPOOLP256R1, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_ecdh_brainpoolp320r1" -> Pair(EcCurve.BRAINPOOLP320R1, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_ecdh_brainpoolp384r1" -> Pair(EcCurve.BRAINPOOLP384R1, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_ecdh_brainpoolp512r1" -> Pair(EcCurve.BRAINPOOLP512R1, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_x25519" -> Pair(EcCurve.X25519, setOf(KeyPurpose.AGREE_KEY)) - "devmode_sa_software_mdoc_auth_x448" -> Pair(EcCurve.X448, setOf(KeyPurpose.AGREE_KEY)) - else -> throw IllegalStateException() - } - var passphrase: String? = null - val passphraseConstraints = when ((collectedEvidence["devmode_sa_software_passphrase_complexity"] - as EvidenceResponseQuestionMultipleChoice).answerId) { - "devmode_sa_software_passphrase_none" -> null - "devmode_sa_software_passphrase_6_digit_pin" -> { - passphrase = (collectedEvidence["devmode_sa_software_passphrase"] - as EvidenceResponseCreatePassphrase).passphrase - PassphraseConstraints.PIN_SIX_DIGITS - } - "devmode_sa_software_passphrase_8_char_or_longer_passphrase" -> { - passphrase = (collectedEvidence["devmode_sa_software_passphrase"] - as EvidenceResponseCreatePassphrase).passphrase - PassphraseConstraints(8, Int.MAX_VALUE, false) - } - else -> throw IllegalStateException() - } - val builder = CborMap.builder() - .put("curve", curve.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(purposes)) - if (passphrase != null) { - builder.put("passphrase", passphrase) - } - if (passphraseConstraints != null) { - builder.put("passphraseConstraints", passphraseConstraints.toDataItem()) - } - return CredentialConfiguration( - challenge, - "SoftwareSecureArea", - Cbor.encode(builder.end().build()) - ) - } - else -> { - throw IllegalStateException("Unexpected value $chosenSa") - } - } - } - - private fun createDocumentConfiguration(collectedEvidence: Map?): DocumentConfiguration { - val cardArt = pngData(application.applicationContext, R.drawable.utopia_driving_license_card_art) - - if (collectedEvidence == null) { - return DocumentConfiguration( - displayName = resourceString(R.string.utopia_mdl_issuing_authority_pending_document_title), - typeDisplayName = "Driving License", - cardArt = cardArt, - requireUserAuthenticationToViewDocument = true, - mdocConfiguration = null, - sdJwtVcDocumentConfiguration = null, - ) - } - - val staticData: NameSpacedData - - val now = Clock.System.now() - val issueDate = now - val expiryDate = now + 365.days * 5 - - val credType = application.documentTypeRepository.getDocumentTypeForMdoc(MDL_DOCTYPE)!! - - val path = (collectedEvidence["path"] as EvidenceResponseQuestionMultipleChoice).answerId - if (path == "hardcoded") { - val imageFormat = collectedEvidence["devmode_image_format"] - val jpeg2k = imageFormat is EvidenceResponseQuestionMultipleChoice && - imageFormat.answerId == "devmode_image_format_jpeg2000" - staticData = getSampleData(jpeg2k, credType).build() - } else { - val icaoPassiveData = collectedEvidence["passive"] - val icaoTunnelData = collectedEvidence["tunnel"] - val mrtdData = if (icaoTunnelData is EvidenceResponseIcaoNfcTunnelResult) - MrtdNfcData(icaoTunnelData.dataGroups, icaoTunnelData.securityObject) - else if (icaoPassiveData is EvidenceResponseIcaoPassiveAuthentication) - MrtdNfcData(icaoPassiveData.dataGroups, icaoPassiveData.securityObject) - else - throw IllegalStateException("Should not happen") - val decoder = MrtdNfcDataDecoder() - val decoded = decoder.decode(mrtdData) - val firstName = decoded.firstName - val lastName = decoded.lastName - val sex = when (decoded.gender) { - "MALE" -> 1L - "FEMALE" -> 2L - else -> 0L - } - val timeZone = TimeZone.currentSystemDefault() - val dateOfBirth = LocalDate.parse(input = decoded.dateOfBirth, - format = LocalDate.Format { - // date of birth cannot be in future - yearTwoDigits(now.toLocalDateTime(timeZone).year - 99) - monthNumber() - dayOfMonth() - }) - val dateOfBirthInstant = dateOfBirth.atStartOfDayIn(timeZone) - // over 18/21 is calculated purely based on calendar date (not based on the birth time zone) - val ageOver18 = now > dateOfBirthInstant.plus(18, DateTimeUnit.YEAR, timeZone) - val ageOver21 = now > dateOfBirthInstant.plus(21, DateTimeUnit.YEAR, timeZone) - val portrait = decoded.photo?.toByteArray() ?: bitmapData(R.drawable.img_erika_portrait) - val signatureOrUsualMark = decoded.signature?.toByteArray() ?: bitmapData(R.drawable.img_erika_signature) - - // Make sure we set at least all the mandatory data elements - // - staticData = NameSpacedData.Builder() - .putEntryString(MDL_NAMESPACE, "given_name", firstName) - .putEntryString(MDL_NAMESPACE, "family_name", lastName) - .putEntry(MDL_NAMESPACE, "birth_date", - Cbor.encode(dateOfBirth.toDataItemFullDate())) - .putEntryByteString(MDL_NAMESPACE, "portrait", portrait) - .putEntryByteString(MDL_NAMESPACE, "signature_usual_mark", signatureOrUsualMark) - .putEntryNumber(MDL_NAMESPACE, "sex", sex) - .putEntry(MDL_NAMESPACE, "issue_date", - Cbor.encode(issueDate.toDataItemDateTimeString())) - .putEntry(MDL_NAMESPACE, "expiry_date", - Cbor.encode(expiryDate.toDataItemDateTimeString()) - ) - .putEntryString(MDL_NAMESPACE, "issuing_authority", - resourceString(R.string.utopia_mdl_issuing_authority_name),) - .putEntryString(MDL_NAMESPACE, "issuing_country", "ZZ") - .putEntryString(MDL_NAMESPACE, "un_distinguishing_sign", "UTO") - .putEntryString(MDL_NAMESPACE, "document_number", "1234567890") - .putEntryString(MDL_NAMESPACE, "administrative_number", "123456789") - .putEntry(MDL_NAMESPACE, "driving_privileges", - Cbor.encode(CborArray.builder().end().build())) - - .putEntryBoolean(MDL_NAMESPACE, "age_over_18", ageOver18) - .putEntryBoolean(MDL_NAMESPACE, "age_over_21", ageOver21) - - .putEntryString(AAMVA_NAMESPACE, "DHS_compliance", "F") - .putEntryNumber(AAMVA_NAMESPACE, "EDL_credential", 1) - .putEntryNumber(AAMVA_NAMESPACE, "sex", sex) - .build() - } - - val firstName = staticData.getDataElementString(MDL_NAMESPACE, "given_name") - return DocumentConfiguration( - displayName = resourceString(R.string.utopia_mdl_issuing_authority_document_title, firstName), - typeDisplayName = "Driving License", - cardArt = cardArt, - requireUserAuthenticationToViewDocument = true, - mdocConfiguration = MdocDocumentConfiguration( - docType = MDL_DOCTYPE, - staticData = staticData, - ), - sdJwtVcDocumentConfiguration = null, - ) - } - - override fun developerModeRequestUpdate(currentConfiguration: DocumentConfiguration): DocumentConfiguration { - // The update consists of just slapping an extra 0 at the end of `administrative_number` - val newAdministrativeNumber = try { - currentConfiguration.mdocConfiguration!!.staticData - .getDataElementString(MDL_NAMESPACE, "administrative_number") - } catch (e: Throwable) { - "" - } + "0" - - - val builder = NameSpacedData.Builder(currentConfiguration.mdocConfiguration!!.staticData) - builder.putEntryString( - MDL_NAMESPACE, - "administrative_number", - newAdministrativeNumber - ) - - return DocumentConfiguration( - displayName = currentConfiguration.displayName, - typeDisplayName = "Driving License", - cardArt = currentConfiguration.cardArt, - requireUserAuthenticationToViewDocument = true, - mdocConfiguration = MdocDocumentConfiguration( - docType = currentConfiguration.mdocConfiguration!!.docType, - staticData = builder.build(), - ), - sdJwtVcDocumentConfiguration = null, - ) - } - - private fun getSampleData(jpeg2k: Boolean, documentType: DocumentType): NameSpacedData.Builder { - val portrait = if (jpeg2k) { - resourceBytes(R.raw.img_erika_portrait) - } else { - bitmapData(R.drawable.img_erika_portrait) - } - val signatureOrUsualMark = if (jpeg2k) { - resourceBytes(R.raw.img_erika_signature) - } else { - bitmapData(R.drawable.img_erika_signature) - } - val builder = NameSpacedData.Builder() - for ((namespaceName, namespace) in documentType.mdocDocumentType!!.namespaces) { - for ((dataElementName, dataElement) in namespace.dataElements) { - if (dataElement.attribute.sampleValue != null) { - builder.putEntry( - namespaceName, - dataElementName, - Cbor.encode(dataElement.attribute.sampleValue!!) - ) - } - } - } - // Sample data currently doesn't have portrait or signature_usual_mark - builder - .putEntryByteString(MDL_NAMESPACE, "portrait", portrait) - .putEntryByteString(MDL_NAMESPACE, "signature_usual_mark", signatureOrUsualMark) - return builder - } -} \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt index 0af009caa..3fe4ebce9 100644 --- a/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt +++ b/wallet/src/main/java/com/android/identity_credential/wallet/ui/destination/provisioncredential/EvidenceRequest.kt @@ -1654,7 +1654,7 @@ fun EvidenceRequestWebView( val scope = rememberCoroutineScope() LaunchedEffect(url, redirectUri) { // NB: these scopes will be cancelled when navigating outside of this screen. - val appSupport = walletServerProvider.getApplicationSupport() + val appSupport = walletServerProvider.getApplicationSupportConnection().applicationSupport scope.launch { // Wait for notifications appSupport.notifications.collectLatest { notification -> diff --git a/wallet/src/main/res/raw/csa_certificate.pem b/wallet/src/main/res/raw/csa_certificate.pem new file mode 100644 index 000000000..18d2599bb --- /dev/null +++ b/wallet/src/main/res/raw/csa_certificate.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBUzCB+qADAgECAgkAjXAZuiLhFG8wCgYIKoZIzj0EAwIwFzEVMBMGA1UEAwwM +Y3NhX2Rldl9yb290MB4XDTI0MTExMzAwMDcwM1oXDTM0MTEyMTAwMDcwM1owFzEV +MBMGA1UEAwwMY3NhX2Rldl9yb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +SwFeYEYGbPrayIwwCFeOgNkgfuJZViqnX0GHAyJ2aAtA4Pm88Txy4gKWkIeAw12v +JEfq75Y4WVBTU3mDppPTxKMvMC0wHQYDVR0OBBYEFEqc1iDkhWpfhozT8rxG49A6 +ClfbMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDSAAwRQIgM75KRzV6I4gARk1I +BcCX7n+0r2OEXWfF67N0lHcKNjYCIQDQssA1bu0juNn+GXQNmc0CVhdSAF2JbRc+ +71j/NYLI4w== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthority.kt b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthority.kt similarity index 98% rename from wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthority.kt rename to wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthority.kt index cb7f2b8f8..c06a466b4 100644 --- a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthority.kt +++ b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthority.kt @@ -61,10 +61,6 @@ abstract class SimpleIssuingAuthority( // This can be changed to simulate proofing and requesting CPOs being slow. protected var delayForProofingAndIssuance: Duration = 1.seconds - open fun createNfcTunnelHandler(): SimpleIcaoNfcTunnelDriver { - throw UnsupportedOperationException("Tunnel not supported") - } - // If issuing authority has NFC card access data (such as PIN or CAN code - // probably from already-collected evidence) return it here. This is required // to avoid scanning passport/id card MRZ strip with camera. @@ -266,8 +262,7 @@ abstract class SimpleIssuingAuthority( return SimpleIssuingAuthorityProofingFlow( this, documentId, - getProofingGraphRoot(issuerDocument.registrationResponse), - this::createNfcTunnelHandler + getProofingGraphRoot(issuerDocument.registrationResponse) ) } @@ -385,7 +380,8 @@ abstract class SimpleIssuingAuthority( // Skip if we already have a request for the authentication key if (hasCpoRequestForAuthenticationKey(issuerDocument, request.secureAreaBoundKeyAttestation.publicKey)) { - Logger.d(TAG, "Already has cpoRequest for attestation with key " + + Logger.d( + TAG, "Already has cpoRequest for attestation with key " + "${request.secureAreaBoundKeyAttestation.publicKey}") continue } diff --git a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingFlow.kt b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingFlow.kt similarity index 65% rename from wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingFlow.kt rename to wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingFlow.kt index f34733a15..31d4d5dd1 100644 --- a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingFlow.kt +++ b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingFlow.kt @@ -12,11 +12,9 @@ import java.lang.IllegalStateException class SimpleIssuingAuthorityProofingFlow( private val issuingAuthority: SimpleIssuingAuthority, private val documentId: String, - private var currentNode: SimpleIssuingAuthorityProofingGraph.Node?, - private val tunnelDriverFactory: (() -> SimpleIcaoNfcTunnelDriver)? = null + private var currentNode: SimpleIssuingAuthorityProofingGraph.Node? ) : ProofingFlow { private var pendingTunnelRequest: EvidenceRequestIcaoNfcTunnel? = null - private var nfcTunnel: SimpleIcaoNfcTunnelDriver? = null companion object { private const val TAG = "SimpleIssuingAuthorityProofingFlow" @@ -28,33 +26,14 @@ class SimpleIssuingAuthorityProofingFlow( this.pendingTunnelRequest = null return listOf(pendingTunnelRequest) } - val currentNode = this.currentNode - if (currentNode == null) { - return emptyList() - } + val currentNode = this.currentNode ?: return emptyList() return currentNode.requests } override suspend fun sendEvidence(evidenceResponse: EvidenceResponse) { Logger.d(TAG, "Receiving evidence $evidenceResponse") val evidence = if (evidenceResponse is EvidenceResponseIcaoNfcTunnel) { - if (nfcTunnel == null) { - nfcTunnel = tunnelDriverFactory!!() - val dataGroups = (currentNode as SimpleIssuingAuthorityProofingGraph.IcaoNfcTunnelNode).dataGroups - nfcTunnel!!.init(dataGroups, issuingAuthority.getMrtdAccessData(documentId)) - } - val tunnel = nfcTunnel!! - // This is special case - val nextRequest = tunnel.handleNfcTunnelResponse(evidenceResponse) - if (nextRequest == null) { - // end if tunnel workflow; do not send to the client, instead save collected - // evidence and move on to the next node in the evidence collection graph. - nfcTunnel = null - tunnel.collectEvidence() - } else { - this.pendingTunnelRequest = nextRequest - return - } + throw IllegalArgumentException("NFC tunnel not supported") } else { evidenceResponse } diff --git a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingGraph.kt b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingGraph.kt similarity index 100% rename from wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingGraph.kt rename to wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityProofingGraph.kt diff --git a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRegistrationFlow.kt b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRegistrationFlow.kt similarity index 100% rename from wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRegistrationFlow.kt rename to wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRegistrationFlow.kt diff --git a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt similarity index 87% rename from wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt rename to wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt index f83d78e0c..13151d7a9 100644 --- a/wallet/src/main/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt +++ b/wallet/src/test/java/com/android/identity/issuance/simple/SimpleIssuingAuthorityRequestCredentialsFlow.kt @@ -1,6 +1,7 @@ package com.android.identity.issuance.simple import com.android.identity.cbor.DataItem +import com.android.identity.device.DeviceAssertion import com.android.identity.issuance.CredentialConfiguration import com.android.identity.issuance.CredentialFormat import com.android.identity.issuance.CredentialRequest @@ -20,7 +21,10 @@ class SimpleIssuingAuthorityRequestCredentialsFlow( return credentialConfiguration } - override suspend fun sendCredentials(credentialRequests: List): List { + override suspend fun sendCredentials( + credentialRequests: List, + keysAssertion: DeviceAssertion + ): List { // TODO: should check attestations issuingAuthority.addCpoRequests(documentId, format, credentialRequests) return emptyList() diff --git a/wallet/src/test/java/com/android/identity_credential/wallet/SelfSignedMdlTest.kt b/wallet/src/test/java/com/android/identity_credential/wallet/SelfSignedMdlTest.kt index bf398505f..89449dfb4 100644 --- a/wallet/src/test/java/com/android/identity_credential/wallet/SelfSignedMdlTest.kt +++ b/wallet/src/test/java/com/android/identity_credential/wallet/SelfSignedMdlTest.kt @@ -1,5 +1,6 @@ package com.android.identity_credential.wallet +import com.android.identity.device.DeviceAssertion import com.android.identity.issuance.RegistrationResponse import com.android.identity.issuance.DocumentCondition import com.android.identity.issuance.CredentialFormat @@ -15,6 +16,7 @@ import com.android.identity.securearea.CreateKeySettings import com.android.identity.securearea.software.SoftwareSecureArea import com.android.identity.storage.EphemeralStorageEngine import kotlinx.coroutines.test.runTest +import kotlinx.io.bytestring.ByteString import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Assert import org.junit.Before @@ -27,27 +29,27 @@ class SelfSignedMdlTest { Security.insertProviderAt(BouncyCastleProvider(), 1) } - private fun getProofingQuestions() : List { - return listOf( - EvidenceRequestMessage( + private fun getProofingQuestions() : List { + return listOf( + com.android.identity.issuance.evidence.EvidenceRequestMessage( "Here's a long string with TOS", mapOf(), "Accept", "Do Not Accept", ), - EvidenceRequestQuestionString( + com.android.identity.issuance.evidence.EvidenceRequestQuestionString( "What first name should be used for the mDL?", mapOf(), "Erika", "Continue", ), - EvidenceRequestQuestionMultipleChoice( + com.android.identity.issuance.evidence.EvidenceRequestQuestionMultipleChoice( "Select the card art for the credential", mapOf(), mapOf("green" to "Green", "blue" to "Blue", "red" to "Red"), "Continue", ), - EvidenceRequestMessage( + com.android.identity.issuance.evidence.EvidenceRequestMessage( "Your application is about to be sent the ID issuer for " + "verification. You will get notified when the " + "application is approved.", @@ -69,13 +71,13 @@ class SelfSignedMdlTest { val registerdocumentFlow = ia.register() val registrationConfiguration = registerdocumentFlow.getDocumentRegistrationConfiguration() val documentId = registrationConfiguration.documentId - val registrationResponse = RegistrationResponse(true) + val registrationResponse = com.android.identity.issuance.RegistrationResponse(true) registerdocumentFlow.sendDocumentRegistrationResponse(registrationResponse) registerdocumentFlow.complete() // Check we're now in the proofing state. Assert.assertEquals( - DocumentCondition.PROOFING_REQUIRED, + com.android.identity.issuance.DocumentCondition.PROOFING_REQUIRED, ia.getState(documentId).condition) // Perform proofing @@ -84,44 +86,57 @@ class SelfSignedMdlTest { // First piece of evidence to return... var evidenceToGet = proofingFlow.getEvidenceRequests() Assert.assertEquals(1, evidenceToGet.size) - Assert.assertTrue(evidenceToGet[0] is EvidenceRequestMessage) - Assert.assertEquals("Here's a long string with TOS", (evidenceToGet[0] as EvidenceRequestMessage).message) - proofingFlow.sendEvidence(EvidenceResponseMessage(true)) + Assert.assertTrue(evidenceToGet[0] is com.android.identity.issuance.evidence.EvidenceRequestMessage) + Assert.assertEquals("Here's a long string with TOS", (evidenceToGet[0] as com.android.identity.issuance.evidence.EvidenceRequestMessage).message) + proofingFlow.sendEvidence( + com.android.identity.issuance.evidence.EvidenceResponseMessage( + true + ) + ) // Second piece of evidence to return... evidenceToGet = proofingFlow.getEvidenceRequests() Assert.assertEquals(1, evidenceToGet.size) - Assert.assertTrue(evidenceToGet[0] is EvidenceRequestQuestionString) + Assert.assertTrue(evidenceToGet[0] is com.android.identity.issuance.evidence.EvidenceRequestQuestionString) Assert.assertEquals( "What first name should be used for the mDL?", - (evidenceToGet[0] as EvidenceRequestQuestionString).message) + (evidenceToGet[0] as com.android.identity.issuance.evidence.EvidenceRequestQuestionString).message) Assert.assertEquals( "Erika", - (evidenceToGet[0] as EvidenceRequestQuestionString).defaultValue) - proofingFlow.sendEvidence(EvidenceResponseQuestionString("Max")) + (evidenceToGet[0] as com.android.identity.issuance.evidence.EvidenceRequestQuestionString).defaultValue) + proofingFlow.sendEvidence( + com.android.identity.issuance.evidence.EvidenceResponseQuestionString( + "Max" + ) + ) Assert.assertEquals( - DocumentCondition.PROOFING_REQUIRED, + com.android.identity.issuance.DocumentCondition.PROOFING_REQUIRED, ia.getState(documentId).condition) // Third piece of evidence to return... evidenceToGet = proofingFlow.getEvidenceRequests() Assert.assertEquals(1, evidenceToGet.size) - Assert.assertTrue(evidenceToGet[0] is EvidenceRequestQuestionMultipleChoice) + Assert.assertTrue(evidenceToGet[0] is com.android.identity.issuance.evidence.EvidenceRequestQuestionMultipleChoice) Assert.assertEquals(3, - (evidenceToGet[0] as EvidenceRequestQuestionMultipleChoice).possibleValues.size) + (evidenceToGet[0] as com.android.identity.issuance.evidence.EvidenceRequestQuestionMultipleChoice).possibleValues.size) proofingFlow.sendEvidence( - EvidenceResponseQuestionMultipleChoice( - (evidenceToGet[0] as EvidenceRequestQuestionMultipleChoice).possibleValues.keys.iterator().next() - ) + com.android.identity.issuance.evidence.EvidenceResponseQuestionMultipleChoice( + (evidenceToGet[0] as com.android.identity.issuance.evidence.EvidenceRequestQuestionMultipleChoice).possibleValues.keys.iterator() + .next() + ) ) // Fourth piece of evidence to return... evidenceToGet = proofingFlow.getEvidenceRequests() Assert.assertEquals(1, evidenceToGet.size) - Assert.assertTrue(evidenceToGet[0] is EvidenceRequestMessage) - Assert.assertTrue((evidenceToGet[0] as EvidenceRequestMessage).message + Assert.assertTrue(evidenceToGet[0] is com.android.identity.issuance.evidence.EvidenceRequestMessage) + Assert.assertTrue((evidenceToGet[0] as com.android.identity.issuance.evidence.EvidenceRequestMessage).message .startsWith("Your application is about to be sent")) - proofingFlow.sendEvidence(EvidenceResponseMessage(true)) + proofingFlow.sendEvidence( + EvidenceResponseMessage( + true + ) + ) // Check there are no more pieces of evidence to return and it's now processing // after we signal that proofing is complete @@ -130,44 +145,47 @@ class SelfSignedMdlTest { proofingFlow.complete() Assert.assertEquals( - DocumentCondition.PROOFING_PROCESSING, + com.android.identity.issuance.DocumentCondition.PROOFING_PROCESSING, ia.getState(documentId).condition) // Processing is hard-coded to take three seconds Thread.sleep(2100) Assert.assertEquals( - DocumentCondition.PROOFING_PROCESSING, + com.android.identity.issuance.DocumentCondition.PROOFING_PROCESSING, ia.getState(documentId).condition) Thread.sleep(900) Assert.assertEquals( - DocumentCondition.CONFIGURATION_AVAILABLE, + com.android.identity.issuance.DocumentCondition.CONFIGURATION_AVAILABLE, ia.getState(documentId).condition) // Check we can get the credential configuration val configuration = ia.getDocumentConfiguration(documentId) Assert.assertEquals("Max's Driving License", configuration.displayName) Assert.assertEquals( - DocumentCondition.READY, + com.android.identity.issuance.DocumentCondition.READY, ia.getState(documentId).condition) // Check we can get CPOs, first request them val numMso = 5 val requestCpoFlow = ia.requestCredentials(documentId) - val authKeyConfiguration = requestCpoFlow.getCredentialConfiguration(CredentialFormat.MDOC_MSO) - val credentialRequests = mutableListOf() + val authKeyConfiguration = requestCpoFlow.getCredentialConfiguration(com.android.identity.issuance.CredentialFormat.MDOC_MSO) + val credentialRequests = mutableListOf() for (authKeyNumber in IntRange(0, numMso - 1)) { val alias = "AuthKey_$authKeyNumber" secureArea.createKey(alias, CreateKeySettings()) credentialRequests.add( - CredentialRequest(secureArea.getKeyInfo(alias).attestation) + com.android.identity.issuance.CredentialRequest(secureArea.getKeyInfo(alias).attestation) ) } - requestCpoFlow.sendCredentials(credentialRequests) + requestCpoFlow.sendCredentials( + credentialRequests, + DeviceAssertion(ByteString(), ByteString()) + ) requestCpoFlow.complete() // documentInformation should now reflect that the CPOs are pending and not // yet available.. ia.getState(documentId).let { - Assert.assertEquals(DocumentCondition.READY, it.condition) + Assert.assertEquals(com.android.identity.issuance.DocumentCondition.READY, it.condition) Assert.assertEquals(5, it.numPendingCredentials) Assert.assertEquals(0, it.numAvailableCredentials) } @@ -175,7 +193,7 @@ class SelfSignedMdlTest { Thread.sleep(100) // Still not available... ia.getState(documentId).let { - Assert.assertEquals(DocumentCondition.READY, it.condition) + Assert.assertEquals(com.android.identity.issuance.DocumentCondition.READY, it.condition) Assert.assertEquals(5, it.numPendingCredentials) Assert.assertEquals(0, it.numAvailableCredentials) } @@ -183,7 +201,7 @@ class SelfSignedMdlTest { // But it is available after 3 seconds Thread.sleep(2900) ia.getState(documentId).let { - Assert.assertEquals(DocumentCondition.READY, it.condition) + Assert.assertEquals(com.android.identity.issuance.DocumentCondition.READY, it.condition) Assert.assertEquals(0, it.numPendingCredentials) Assert.assertEquals(5, it.numAvailableCredentials) } @@ -200,7 +218,7 @@ class SelfSignedMdlTest { // Once we collected them, they are no longer available to be collected // and nothing is pending ia.getState(documentId).let { - Assert.assertEquals(DocumentCondition.READY, it.condition) + Assert.assertEquals(com.android.identity.issuance.DocumentCondition.READY, it.condition) Assert.assertEquals(0, it.numPendingCredentials) Assert.assertEquals(0, it.numAvailableCredentials) } diff --git a/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt b/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt index 92cff3ef6..62abbd322 100644 --- a/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt +++ b/wallet/src/test/java/com/android/identity_credential/wallet/TestIssuingAuthority.kt @@ -1,23 +1,18 @@ package com.android.identity_credential.wallet -import com.android.identity.cbor.Cbor -import com.android.identity.cbor.CborMap -import com.android.identity.crypto.EcCurve import com.android.identity.document.NameSpacedData import com.android.identity.crypto.EcPublicKey import com.android.identity.issuance.CredentialConfiguration -import com.android.identity.issuance.DocumentConfiguration -import com.android.identity.issuance.CredentialFormat -import com.android.identity.issuance.RegistrationResponse import com.android.identity.issuance.IssuingAuthorityConfiguration import com.android.identity.issuance.MdocDocumentConfiguration import com.android.identity.issuance.evidence.EvidenceResponse import com.android.identity.issuance.evidence.EvidenceResponseQuestionString import com.android.identity.issuance.simple.SimpleIssuingAuthority import com.android.identity.issuance.simple.SimpleIssuingAuthorityProofingGraph -import com.android.identity.securearea.KeyPurpose import com.android.identity.storage.EphemeralStorageEngine import com.android.identity.mrtd.MrtdAccessData +import com.android.identity.securearea.config.SecureAreaConfigurationSoftware +import kotlinx.io.bytestring.ByteString import kotlin.time.Duration.Companion.seconds class TestIssuingAuthority: SimpleIssuingAuthority(EphemeralStorageEngine(), {}) { @@ -35,9 +30,9 @@ class TestIssuingAuthority: SimpleIssuingAuthority(EphemeralStorageEngine(), {}) configuration = IssuingAuthorityConfiguration( identifier = "mDL_SelfSigned", issuingAuthorityName = "Test IA", - issuingAuthorityLogo = byteArrayOf(1, 2, 3), + issuingAuthorityLogo = byteArrayOf(1, 2, 3), issuingAuthorityDescription = "mDL from Test IA", - pendingDocumentInformation = DocumentConfiguration( + pendingDocumentInformation = com.android.identity.issuance.DocumentConfiguration( displayName = "mDL for Test IA (proofing pending)", typeDisplayName = "Driving License", cardArt = byteArrayOf(1, 2, 3), @@ -57,23 +52,23 @@ class TestIssuingAuthority: SimpleIssuingAuthority(EphemeralStorageEngine(), {}) delayForProofingAndIssuance = 3.seconds } - override fun createPresentationData(presentationFormat: CredentialFormat, - documentConfiguration: DocumentConfiguration, + override fun createPresentationData(presentationFormat: com.android.identity.issuance.CredentialFormat, + documentConfiguration: com.android.identity.issuance.DocumentConfiguration, authenticationKey: EcPublicKey ): ByteArray { return byteArrayOf(1, 2, 3) } - override fun developerModeRequestUpdate(currentConfiguration: DocumentConfiguration): DocumentConfiguration { + override fun developerModeRequestUpdate(currentConfiguration: com.android.identity.issuance.DocumentConfiguration): com.android.identity.issuance.DocumentConfiguration { return configuration.pendingDocumentInformation } - override suspend fun getConfiguration(): IssuingAuthorityConfiguration { + override suspend fun getConfiguration(): com.android.identity.issuance.IssuingAuthorityConfiguration { return configuration } override fun getProofingGraphRoot( - registrationResponse: RegistrationResponse + registrationResponse: com.android.identity.issuance.RegistrationResponse ): SimpleIssuingAuthorityProofingGraph.Node { return SimpleIssuingAuthorityProofingGraph.create { message( @@ -111,14 +106,14 @@ class TestIssuingAuthority: SimpleIssuingAuthority(EphemeralStorageEngine(), {}) return true } - override fun generateDocumentConfiguration(collectedEvidence: Map): DocumentConfiguration { + override fun generateDocumentConfiguration(collectedEvidence: Map): com.android.identity.issuance.DocumentConfiguration { val firstName = (collectedEvidence["name"] as EvidenceResponseQuestionString).answer - return DocumentConfiguration( + return com.android.identity.issuance.DocumentConfiguration( displayName = "${firstName}'s Driving License", typeDisplayName = "Driving License", cardArt = byteArrayOf(1, 2, 3), requireUserAuthenticationToViewDocument = true, - mdocConfiguration = MdocDocumentConfiguration( + mdocConfiguration = com.android.identity.issuance.MdocDocumentConfiguration( "org.iso.18013.5.1.mDL", NameSpacedData.Builder().build(), ), @@ -126,17 +121,12 @@ class TestIssuingAuthority: SimpleIssuingAuthority(EphemeralStorageEngine(), {}) ) } - override fun createCredentialConfiguration(collectedEvidence: MutableMap): CredentialConfiguration { + override fun createCredentialConfiguration( + collectedEvidence: MutableMap + ): CredentialConfiguration { return CredentialConfiguration( - byteArrayOf(1, 2, 3), - "SoftwareSecureArea", - Cbor.encode( - CborMap.builder() - .put("curve", EcCurve.P256.coseCurveIdentifier) - .put("purposes", KeyPurpose.encodeSet(setOf(KeyPurpose.SIGN))) - .end().build() - ) + ByteString(byteArrayOf(1, 2, 3)), + SecureAreaConfigurationSoftware() ) } - } \ No newline at end of file