From b2774edb2e54ca8f537b8861aa03c5b37d4f63ba Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Wed, 14 Aug 2024 09:32:59 +0200 Subject: [PATCH 1/8] Add sdJwt presentation --- gradle/libs.versions.toml | 4 + wallet-core/build.gradle | 4 + .../eu/europa/ec/eudi/wallet/EudiWallet.kt | 101 ++++++-- .../wallet/issue/openid4vci/DefaultOffer.kt | 8 +- .../issue/openid4vci/DocumentManagerSdJwt.kt | 93 ++++++++ .../wallet/issue/openid4vci/OfferCreator.kt | 14 +- .../wallet/issue/openid4vci/OfferResolver.kt | 3 +- .../issue/openid4vci/ProcessResponse.kt | 113 ++++++++- .../ec/eudi/wallet/keystore/KeyGenerator.kt | 145 ++++++++++++ .../transfer/openid4vp/OpenId4VpRequest.kt | 8 +- .../transfer/openid4vp/OpenId4vpManager.kt | 149 +++++++++--- .../OpenId4VpCBORResponseGeneratorImpl.kt | 17 +- .../OpenId4VpResponseGeneratorDelegator.kt | 112 +++++++++ .../OpenId4VpSdJwtResponseGeneratorImpl.kt | 192 ++++++++++++++++ .../wallet/util/EncryptedSharedPrefsUtil.kt | 39 ++++ .../openid4vci/IssuerAuthorizationTest.kt | 217 ------------------ 16 files changed, 908 insertions(+), 311 deletions(-) create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt rename wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/{ => responseGenerator}/OpenId4VpCBORResponseGeneratorImpl.kt (95%) create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/EncryptedSharedPrefsUtil.kt delete mode 100644 wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc82c307..3729948c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ eudi-document-manager = "0.4.1" eudi-iso18013-data-transfer = "0.2.0" eudi-lib-jvm-openid4vci-kt = "dpopNonce-SNAPSHOT" eudi-lib-jvm-siop-openid4vp-kt = "0.4.2" +eudiLibJvmSdjwtKt = "0.5.1" gradle-plugin = "7.4.0" identity-credential = "20231002" identity-credential-android = "20231002" @@ -26,6 +27,7 @@ mockito-android = "4.0.0" mockito-inline = "4.0.0" mockk = "1.13.5" nimbus-sdk = "11.8" +securityCrypto = "1.1.0-alpha06" sonarqube = "4.4.1.3373" test-core = "1.4.0" test-rules = "1.4.0" @@ -48,6 +50,7 @@ espresso-intents = { module = "androidx.test.espresso:espresso-intents", version eudi-document-manager = { module = "eu.europa.ec.eudi:eudi-lib-android-wallet-document-manager", version.ref = "eudi-document-manager" } eudi-iso18013-data-transfer = { module = "eu.europa.ec.eudi:eudi-lib-android-iso18013-data-transfer", version.ref = "eudi-iso18013-data-transfer" } eudi-lib-jvm-openid4vci-kt = { module = "com.github.TICESoftware:eudi-lib-jvm-openid4vci-kt", version.ref = "eudi-lib-jvm-openid4vci-kt" } +eudi-lib-jvm-sdjwt-kt = { module = "eu.europa.ec.eudi:eudi-lib-jvm-sdjwt-kt", version.ref = "eudiLibJvmSdjwtKt" } eudi-lib-jvm-siop-openid4vp-kt = { module = "eu.europa.ec.eudi:eudi-lib-jvm-siop-openid4vp-kt", version.ref = "eudi-lib-jvm-siop-openid4vp-kt" } identity-credential = { module = "com.android.identity:identity-credential", version.ref = "identity-credential" } json = { module = "org.json:json", version.ref = "json" } @@ -60,6 +63,7 @@ mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-inline" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } nimbus-oauth2-oidc-sdk = { module = "com.nimbusds:oauth2-oidc-sdk", version.ref = "nimbus-sdk" } +security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" } test-core = { module = "androidx.test:core", version.ref = "test-core" } test-coreKtx = { module = "androidx.test:core-ktx", version.ref = "test-core" } test-rules = { module = "androidx.test:rules", version.ref = "test-rules" } diff --git a/wallet-core/build.gradle b/wallet-core/build.gradle index 038cdc7e..1aa017bc 100644 --- a/wallet-core/build.gradle +++ b/wallet-core/build.gradle @@ -158,6 +158,10 @@ dependencies { api libs.upokecenter.cbor api libs.cose.java + //sdjwt + api libs.eudi.lib.jvm.sdjwt.kt + implementation libs.security.crypto + testImplementation libs.junit testImplementation libs.junit.jupiter.params testImplementation libs.json diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt index 04110600..04aaf6bb 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt @@ -35,9 +35,11 @@ import eu.europa.ec.eudi.wallet.document.sample.SampleDocumentManager import eu.europa.ec.eudi.wallet.internal.getCertificate import eu.europa.ec.eudi.wallet.internal.mainExecutor import eu.europa.ec.eudi.wallet.issue.openid4vci.* +import eu.europa.ec.eudi.wallet.keystore.KeyGenerator +import eu.europa.ec.eudi.wallet.keystore.KeyGeneratorImpl import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpCBORResponse -import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpCBORResponseGeneratorImpl import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4vpManager +import eu.europa.ec.eudi.wallet.transfer.openid4vp.responseGenerator.OpenId4VpResponseGeneratorDelegator import eu.europa.ec.eudi.wallet.util.DefaultNfcEngagementService import java.security.cert.X509Certificate import java.util.concurrent.Executor @@ -68,7 +70,7 @@ import java.util.concurrent.Executor * */ @SuppressLint("StaticFieldLeak") -object EudiWallet { +object EudiWallet : KeyGenerator by KeyGeneratorImpl { @Volatile private lateinit var context: Context @@ -88,6 +90,7 @@ object EudiWallet { fun init(context: Context, config: EudiWalletConfig) { this.context = context.applicationContext this._config = config + DocumentManagerSdJwt.init(context, config.userAuthenticationRequired) } /** @@ -197,7 +200,9 @@ object EudiWallet { * @throws IllegalStateException if [EudiWallet] is not firstly initialized via the [init] method */ fun deleteDocumentById(documentId: DocumentId): DeleteDocumentResult = - documentManager.deleteDocumentById(documentId) + documentManager.deleteDocumentById(documentId).apply { + if (this is DeleteDocumentResult.Success) DocumentManagerSdJwt.deleteDocument(documentId) + } /** * Create an [UnsignedDocument] for the given [docType] @@ -224,7 +229,10 @@ object EudiWallet { * @return [StoreDocumentResult] * @throws IllegalStateException if [EudiWallet] is not firstly initialized via the [init] method */ - fun storeIssuedDocument(unsignedDocument: UnsignedDocument, data: ByteArray): StoreDocumentResult = + fun storeIssuedDocument( + unsignedDocument: UnsignedDocument, + data: ByteArray + ): StoreDocumentResult = documentManager.storeIssuedDocument(unsignedDocument, data) private var openId4VciManager: OpenId4VciManager? = null @@ -280,7 +288,15 @@ object EudiWallet { config(config) logger = this@EudiWallet.logger ktorHttpClientFactory = _config.ktorHttpClientFactory - }.also { it.issueDocumentByDocType(docType, txCode, executor, authorizationHandler, onEvent) } + }.also { + it.issueDocumentByDocType( + docType, + txCode, + executor, + authorizationHandler, + onEvent + ) + } } ?: run { (executor ?: context.mainExecutor()).execute { onEvent(IssueEvent.failure(IllegalStateException("OpenId4Vci config is not set in configuration"))) @@ -316,7 +332,15 @@ object EudiWallet { config(config) logger = this@EudiWallet.logger ktorHttpClientFactory = _config.ktorHttpClientFactory - }.also { it.issueDocumentByOffer(offer, txCode, executor, authorizationHandler, onEvent) } + }.also { + it.issueDocumentByOffer( + offer, + txCode, + executor, + authorizationHandler, + onEvent + ) + } } ?: run { (executor ?: context.mainExecutor()).execute { onEvent(IssueEvent.failure(IllegalStateException("OpenId4Vci config is not set in configuration"))) @@ -351,7 +375,15 @@ object EudiWallet { config(config) logger = this@EudiWallet.logger ktorHttpClientFactory = _config.ktorHttpClientFactory - }.also { it.issueDocumentByOfferUri(offerUri, txCode, executor, authorizationHandler, onEvent) } + }.also { + it.issueDocumentByOfferUri( + offerUri, + txCode, + executor, + authorizationHandler, + onEvent + ) + } } ?: run { (executor ?: context.mainExecutor()).execute { onEvent(IssueEvent.failure(IllegalStateException("OpenId4Vci config is not set in configuration"))) @@ -383,7 +415,12 @@ object EudiWallet { ktorHttpClientFactory = _config.ktorHttpClientFactory }.also { when (val document = documentManager.getDocumentById(documentId)) { - is DeferredDocument -> it.issueDeferredDocument(document, executor, onResult) + is DeferredDocument -> it.issueDeferredDocument( + document, + executor, + onResult + ) + else -> (executor ?: context.mainExecutor()).execute { onResult( DeferredIssueResult.DocumentFailed( @@ -474,8 +511,16 @@ object EudiWallet { * @return [EudiWallet] */ fun setTrustedReaderCertificates(trustedReaderCertificates: List) = apply { - deviceResponseGenerator.setReaderTrustStore(ReaderTrustStore.getDefault(trustedReaderCertificates)) - openId4VpCBORResponseGenerator.setReaderTrustStore(ReaderTrustStore.getDefault(trustedReaderCertificates)) + deviceResponseGenerator.setReaderTrustStore( + ReaderTrustStore.getDefault( + trustedReaderCertificates + ) + ) + openId4VpCBORResponseGenerator.setReaderTrustStore( + ReaderTrustStore.getDefault( + trustedReaderCertificates + ) + ) } /** @@ -630,9 +675,10 @@ object EudiWallet { // create response val responseResult = when (transferMode) { TransferMode.OPENID4VP -> - openId4vpManager?.responseGenerator?.createResponse(disclosedDocuments) ?: ResponseResult.Failure( - Throwable("Openid4vpManager has not been initialized properly") - ) + openId4vpManager?.responseGenerator?.createResponse(disclosedDocuments) + ?: ResponseResult.Failure( + Throwable("Openid4vpManager has not been initialized properly") + ) TransferMode.ISO_18013_5, TransferMode.REST_API -> transferManager.responseGenerator.createResponse(disclosedDocuments) @@ -645,7 +691,12 @@ object EudiWallet { is ResponseResult.Success -> { when (transferMode) { TransferMode.OPENID4VP -> - openId4vpManager?.sendResponse((responseResult.response as OpenId4VpCBORResponse).deviceResponseBytes) + openId4vpManager?.sendResponse( + when(val result = responseResult.response){ + is OpenId4VpCBORResponse -> result.deviceResponseBytes + is DeviceResponse -> result.deviceResponseBytes + else -> throw Exception() + }) TransferMode.ISO_18013_5, TransferMode.REST_API -> transferManager.sendResponse((responseResult.response as DeviceResponse).deviceResponseBytes) @@ -695,9 +746,23 @@ object EudiWallet { private val transferManagerDocumentsResolver: DocumentsResolver get() = DocumentsResolver { req -> - documentManager.getDocuments(Document.State.ISSUED) + + DocumentManagerSdJwt + .getAllDocuments() +// .filter { doc -> doc.vct == req.docType } + .map { doc -> + RequestDocument( + documentId = doc.id, + docType = doc.vct, + docName = doc.docName, + userAuthentication = doc.requiresUserAuth, + docRequest = req + ) + }.takeIf { it.isNotEmpty() }?.let { return@DocumentsResolver it } + + return@DocumentsResolver documentManager.getDocuments(Document.State.ISSUED) .filterIsInstance() - .filter { doc -> doc.docType == req.docType } +// .filter { doc -> doc.docType == req.docType } .map { doc -> RequestDocument( documentId = doc.id, @@ -721,9 +786,9 @@ object EudiWallet { } } - private val openId4VpCBORResponseGenerator: OpenId4VpCBORResponseGeneratorImpl by lazy { + private val openId4VpCBORResponseGenerator: OpenId4VpResponseGeneratorDelegator by lazy { requireInit { - OpenId4VpCBORResponseGeneratorImpl.Builder(context) + OpenId4VpResponseGeneratorDelegator.Builder(context) .apply { _config.trustedReaderCertificates?.let { readerTrustStore = ReaderTrustStore.getDefault(it) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt index 5da68cb6..814f24e2 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt @@ -30,7 +30,7 @@ import eu.europa.ec.eudi.openid4vci.* */ internal data class DefaultOffer( @JvmSynthetic val credentialOffer: CredentialOffer, - @JvmSynthetic val credentialConfigurationFilter: CredentialConfigurationFilter = CredentialConfigurationFilter.MsoMdocFormatFilter, + @JvmSynthetic val credentialConfigurationFilter: CredentialConfigurationFilter = CredentialConfigurationFilter.SdJwtOrMsoMdocFormatFilter, ) : Offer { private val issuerMetadata: CredentialIssuerMetadata @@ -41,8 +41,8 @@ internal data class DefaultOffer( override val offeredDocuments: List get() = issuerMetadata.credentialConfigurationsSupported - .filterKeys { it in credentialOffer.credentialConfigurationIdentifiers } - .filterValues { credentialConfigurationFilter(it) } + //.filterKeys { it in credentialOffer.credentialConfigurationIdentifiers } + //.filterValues { credentialConfigurationFilter(it) } .map { (id, conf) -> DefaultOfferedDocument(id, conf) } override val txCodeSpec: Offer.TxCodeSpec? @@ -92,7 +92,7 @@ internal val CredentialConfiguration.name: String internal val CredentialConfiguration.docType: String @JvmSynthetic get() = when (this) { is MsoMdocCredential -> docType - is SdJwtVcCredential -> "eu.europa.ec.eudi.pid_jwt_vc_json" + is SdJwtVcCredential -> "eu.europa.ec.eudi.pid.1" else -> "unknown" } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt new file mode 100644 index 00000000..b2d0078d --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt @@ -0,0 +1,93 @@ +package eu.europa.ec.eudi.wallet.issue.openid4vci + +import android.content.Context +import eu.europa.ec.eudi.wallet.document.DocumentId +import eu.europa.ec.eudi.wallet.util.getEncryptedSharedPreferences +import org.json.JSONException +import org.json.JSONObject +import java.util.Base64 + +object DocumentManagerSdJwt { + private lateinit var dataStore: SdJwtDocumentDataStore + + fun init(context: Context, requiresUserAuth: Boolean) { + dataStore = SdJwtDocumentDataStore(context, requiresUserAuth) + } + + fun storeDocument(id: String, credentials: String) { + dataStore.add(id, credentials) + } + + fun getDocumentById(id: String) = dataStore.get(id) + + fun getAllDocuments() = dataStore.getAll() + + fun deleteDocument(documentId: DocumentId) { + // quick but very dirty solution (we decided to only have one document at all times) + deleteAllDocuments() + } + + fun deleteAllDocuments() { + dataStore.deleteAll() + } + +} + +data class SdJwtDocument( + val id: String, + val vct: String, + val docName: String, + val requiresUserAuth: Boolean, + val data: String, +) + +private class SdJwtDocumentDataStore( + context: Context, + val requiresUserAuth: Boolean, +) { + private var sharedPreferences = getEncryptedSharedPreferences(context, PREF_FILE_NAME) + + fun add(id: String, credentials: String) { + sharedPreferences.edit().putString(id, credentials).apply() + } + + fun get(id: String) = sharedPreferences.getString(id, null)?.toDocument(id, requiresUserAuth) + + fun getAll() = sharedPreferences.all.mapNotNull { + (it.value as? String)?.toDocument(it.key, requiresUserAuth) + } + + fun delete(id: String) { + sharedPreferences.edit().remove(id).apply() + } + + fun deleteAll() { + sharedPreferences.edit().clear().apply() + } + + private companion object { + private const val PREF_FILE_NAME = "document_manager_sdjwt_prefs" + } +} + +private fun String.toDocument( + id: String, + requiresUserAuth: Boolean, +) = try { + val payloadString = split(".")[1] + val payloadJson = JSONObject(String(Base64.getUrlDecoder().decode(payloadString))) + + val vct = payloadJson.getString("vct") + val docName = "Personalausweis" + val data = payloadJson.toString() + + SdJwtDocument( + id = id, + vct = vct, + docName = docName, + requiresUserAuth = requiresUserAuth, + data = this, + ) +} catch (_: JSONException) { + null +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt index 10c05ea5..27d0eb4c 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt @@ -21,8 +21,8 @@ import eu.europa.ec.eudi.openid4vci.CredentialOffer import eu.europa.ec.eudi.openid4vci.Issuer import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.Compose import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.DocTypeFilter -import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.SdJwtOrMsoMdocFormatFilter import io.ktor.client.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -40,20 +40,20 @@ internal class OfferCreator( Issuer.metaData(client, credentialIssuerId) } val credentialConfigurationFilter = Compose( - MsoMdocFormatFilter, + SdJwtOrMsoMdocFormatFilter, DocTypeFilter(docType), ProofTypeFilter(config.proofTypes) ) - val credentialConfigurationId = - credentialIssuerMetadata.credentialConfigurationsSupported.filterValues { conf -> - credentialConfigurationFilter(conf) - }.keys.firstOrNull() ?: throw IllegalStateException("No suitable configuration found") +// val credentialConfigurationId = +// credentialIssuerMetadata.credentialConfigurationsSupported.filterValues { conf -> +// credentialConfigurationFilter(conf) +// }.keys.firstOrNull() ?: throw IllegalStateException("No suitable configuration found") val credentialOffer = CredentialOffer( credentialIssuerIdentifier = credentialIssuerId, credentialIssuerMetadata = credentialIssuerMetadata, authorizationServerMetadata = authorizationServerMetadata.first(), - credentialConfigurationIdentifiers = listOf(credentialConfigurationId) + credentialConfigurationIdentifiers = credentialIssuerMetadata.credentialConfigurationsSupported.keys.toList() ) DefaultOffer(credentialOffer, credentialConfigurationFilter) } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferResolver.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferResolver.kt index f9d805c3..c54bacbb 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferResolver.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferResolver.kt @@ -20,6 +20,7 @@ import eu.europa.ec.eudi.openid4vci.CredentialOfferRequestResolver import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.Compose import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.MsoMdocFormatFilter import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.ProofTypeFilter +import eu.europa.ec.eudi.wallet.issue.openid4vci.CredentialConfigurationFilter.Companion.SdJwtOrMsoMdocFormatFilter import io.ktor.client.* import org.jetbrains.annotations.VisibleForTesting @@ -31,7 +32,7 @@ internal class OfferResolver( CredentialOfferRequestResolver(ktorHttpClientFactory) } val credentialConfigurationFilter by lazy { - Compose(MsoMdocFormatFilter, ProofTypeFilter(proofTypes)) + Compose(SdJwtOrMsoMdocFormatFilter, ProofTypeFilter(proofTypes)) } @VisibleForTesting diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt index 02d438a5..806bbda8 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt @@ -16,25 +16,37 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci +import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.jwk.ECKey import eu.europa.ec.eudi.openid4vci.IssuedCredential import eu.europa.ec.eudi.openid4vci.SubmissionOutcome +import eu.europa.ec.eudi.sdjwt.SdJwtVerifier +import eu.europa.ec.eudi.sdjwt.asJwtVerifier import eu.europa.ec.eudi.wallet.document.DocumentId import eu.europa.ec.eudi.wallet.document.DocumentManager import eu.europa.ec.eudi.wallet.document.StoreDocumentResult import eu.europa.ec.eudi.wallet.document.UnsignedDocument import eu.europa.ec.eudi.wallet.issue.openid4vci.IssueEvent.Companion.documentFailed -import eu.europa.ec.eudi.wallet.issue.openid4vci.OpenId4VciManager.Companion.TAG import eu.europa.ec.eudi.wallet.logging.Logger import eu.europa.ec.eudi.wallet.logging.d +import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4vpManager.Companion.TAG import kotlinx.coroutines.CancellableContinuation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.json.Json import org.bouncycastle.util.encoders.Hex +import org.json.JSONObject +import java.io.ByteArrayInputStream import java.io.Closeable -import java.util.* +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.Base64 import kotlin.coroutines.resume + internal class ProcessResponse( val documentManager: DocumentManager, val deferredContextCreator: DeferredContextCreator, @@ -54,7 +66,10 @@ internal class ProcessResponse( } } - suspend fun process(unsignedDocument: UnsignedDocument, outcomeResult: Result) { + suspend fun process( + unsignedDocument: UnsignedDocument, + outcomeResult: Result + ) { try { processSubmittedRequest(unsignedDocument, outcomeResult.getOrThrow()) } catch (e: Throwable) { @@ -81,18 +96,67 @@ internal class ProcessResponse( when (outcome) { is SubmissionOutcome.Success -> when (val credential = outcome.credentials[0]) { is IssuedCredential.Issued -> try { - val cborBytes = Base64.getUrlDecoder().decode(credential.credential) - logger?.d(TAG, "CBOR bytes: ${Hex.toHexString(cborBytes)}") - documentManager.storeIssuedDocument(unsignedDocument, cborBytes) - .notifyListener(unsignedDocument) + if (isSdJwt(credential.credential)) { + val headerString = credential.credential.split(".").first() + val headerJson = + JSONObject(String(Base64.getUrlDecoder().decode(headerString))) + val keyString = + headerJson.getJSONArray("x5c").getString(0).replace("\n", "") + + val pemKey = "-----BEGIN CERTIFICATE-----\n" + + "${keyString}\n" + + "-----END CERTIFICATE-----" + + val certificateFactory: CertificateFactory = + CertificateFactory.getInstance("X.509") + val certificate = + certificateFactory.generateCertificate(ByteArrayInputStream(pemKey.toByteArray())) as X509Certificate + + val ecKey = ECKey.parse(certificate) + val jwtSignatureVerifier = ECDSAVerifier(ecKey).asJwtVerifier() + + CoroutineScope(Dispatchers.IO).launch { + SdJwtVerifier.verifyIssuance( + jwtSignatureVerifier, + credential.credential + ).getOrThrow() + + DocumentManagerSdJwt.storeDocument( + unsignedDocument.id, + credential.credential + ) + listener.invoke( + IssueEvent.DocumentIssued( + unsignedDocument.id, + unsignedDocument.name, + unsignedDocument.docType + ) + ) + } + } else { + val cborBytes = Base64.getUrlDecoder().decode(credential.credential) + + logger?.d(TAG, "CBOR bytes: ${Hex.toHexString(cborBytes)}") + documentManager.storeIssuedDocument( + unsignedDocument, + cborBytes + ).notifyListener(unsignedDocument) + } } catch (e: Throwable) { - documentManager.deleteDocumentById(unsignedDocument.id) + if (isSdJwt(credential.credential)) { + documentManager.deleteDocumentById(unsignedDocument.id) + } else { + DocumentManagerSdJwt.deleteDocument(unsignedDocument.id) + } listener(documentFailed(unsignedDocument, e)) } is IssuedCredential.Deferred -> { val contextToStore = deferredContextCreator.create(credential) - documentManager.storeDeferredDocument(unsignedDocument, contextToStore.toByteArray()) + documentManager.storeDeferredDocument( + unsignedDocument, + contextToStore.toByteArray() + ) .notifyListener(unsignedDocument, isDeferred = true) } } @@ -114,6 +178,16 @@ internal class ProcessResponse( } } + private fun isSdJwt(credential: String): Boolean { + return try { + val headerString = credential.split(".").first() + val headerJson = JSONObject(String(Base64.getUrlDecoder().decode(headerString))) + true + } catch (e: Exception) { + false + } + } + private fun UserAuthRequiredException.toIssueEvent( unsignedDocument: UnsignedDocument, ): IssueEvent.DocumentRequiresUserAuth { @@ -125,14 +199,29 @@ internal class ProcessResponse( ) } - private fun StoreDocumentResult.notifyListener(unsignedDocument: UnsignedDocument, isDeferred: Boolean = false) = + private fun StoreDocumentResult.notifyListener( + unsignedDocument: UnsignedDocument, + isDeferred: Boolean = false + ) = when (this) { is StoreDocumentResult.Success -> { issuedDocumentIds.add(documentId) if (isDeferred) { - listener(IssueEvent.DocumentDeferred(documentId, unsignedDocument.name, unsignedDocument.docType)) + listener( + IssueEvent.DocumentDeferred( + documentId, + unsignedDocument.name, + unsignedDocument.docType + ) + ) } else { - listener(IssueEvent.DocumentIssued(documentId, unsignedDocument.name, unsignedDocument.docType)) + listener( + IssueEvent.DocumentIssued( + documentId, + unsignedDocument.name, + unsignedDocument.docType + ) + ) } } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt new file mode 100644 index 00000000..71da144d --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt @@ -0,0 +1,145 @@ +package eu.europa.ec.eudi.wallet.keystore + +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties +import android.util.Base64 +import androidx.annotation.RequiresApi +import eu.europa.ec.eudi.wallet.keystore.KeyGenerator.SigningKeyConfig +import java.io.IOException +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.NoSuchAlgorithmException +import java.security.NoSuchProviderException +import java.security.PrivateKey +import java.security.Signature +import java.security.SignatureException +import java.security.UnrecoverableEntryException +import java.security.cert.CertificateException + +private const val ANDROID_KEY_STORE = "AndroidKeyStore" +public const val DEV_KEY_ALIAS = "eudi_wallet_dev_key" + +interface KeyGenerator { + @RequiresApi(Build.VERSION_CODES.R) + @Throws(KeyStoreException::class) + fun getSigningKey(config: SigningKeyConfig): KeyStore.PrivateKeyEntry + + @Throws(SignatureException::class) + fun sign(key: PrivateKey, data: ByteArray): String + + data class SigningKeyConfig( + val keyType: Int, + val timeoutSeconds: Int, + ) +} + +internal object KeyGeneratorImpl : KeyGenerator { + @RequiresApi(Build.VERSION_CODES.R) + @Throws(KeyStoreException::class) + override fun getSigningKey(config: SigningKeyConfig): KeyStore.PrivateKeyEntry { + val entry = getKeyStoreEntry(config) + if (entry !is KeyStore.PrivateKeyEntry) throw KeyStoreException("Entry not an instance of a PrivateKeyEntry.") + return entry + } + + private const val SIGNATURE_ALGORITHM = "SHA256withECDSA" + + @Throws(SignatureException::class) + override fun sign( + key: PrivateKey, + data: ByteArray, + ) = try { + Signature + .getInstance(SIGNATURE_ALGORITHM) + .run { + initSign(key) + update(data) + sign() + }.toBase64String() + } catch (exception: NoSuchAlgorithmException) { + throw SignatureException(exception) +// throw SigningException("Signing failed.", exception) + } catch (exception: InvalidKeyException) { + throw SignatureException(exception) +// throw SigningException("Signing failed.", exception) +// } catch (exception: SignatureException) { +// throw SigningException("Signing failed.", exception) + } + + private fun ByteArray.toBase64String() = String(Base64.encode(this, Base64.DEFAULT)) + + @RequiresApi(Build.VERSION_CODES.R) + @Throws(KeyStoreException::class) + private fun getKeyStoreEntry(config: SigningKeyConfig) = try { + val keyStore = getKeyStore() + keyStore.getEntry(DEV_KEY_ALIAS, null).let { + if (it == null) { + generateKey(config) + keyStore.getEntry(DEV_KEY_ALIAS, null)!! + } else { + it + } + } + } catch (exception: KeyStoreException) { + throw KeyStoreException("Get KeyStore entry failed.", exception) + } catch (exception: NoSuchAlgorithmException) { + throw KeyStoreException("Get KeyStore entry failed.", exception) + } catch (exception: UnrecoverableEntryException) { + throw KeyStoreException("Get KeyStore entry failed.", exception) + } catch (exception: NoSuchProviderException) { + throw KeyStoreException("Get KeyStore entry failed.", exception) + } catch (exception: InvalidAlgorithmParameterException) { + throw KeyStoreException("Get KeyStore entry failed.", exception) + } + + @Throws(KeyStoreException::class) + public fun getKeyStore(): KeyStore = try { + KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } + } catch (exception: KeyStoreException) { + throw KeyStoreException("Get KeyStore instance failed.", exception) + } catch (exception: CertificateException) { + throw KeyStoreException("Get KeyStore instance failed.", exception) + } catch (exception: IOException) { + throw KeyStoreException("Get KeyStore instance failed.", exception) + } catch (exception: NoSuchAlgorithmException) { + throw KeyStoreException("Get KeyStore instance failed.", exception) + } + + @RequiresApi(Build.VERSION_CODES.R) + @Throws(KeyStoreException::class) + private fun generateKey(config: SigningKeyConfig) { + val keyPairGenerator: KeyPairGenerator = + try { + KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, + ANDROID_KEY_STORE + ) + } catch (exception: NoSuchAlgorithmException) { + throw KeyStoreException("Generate key failed.", exception) + } catch (exception: NoSuchProviderException) { + throw KeyStoreException("Generate key failed.", exception) + } + val parameterSpec: KeyGenParameterSpec = + KeyGenParameterSpec + .Builder( + DEV_KEY_ALIAS, + KeyProperties.PURPOSE_SIGN, + ).run { + setUserAuthenticationParameters(config.timeoutSeconds, config.keyType) + setUserAuthenticationRequired(true) + setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) + build() + } + + try { + keyPairGenerator.initialize(parameterSpec) + } catch (exception: InvalidAlgorithmParameterException) { + throw KeyStoreException("Generate key failed.", exception) + } + keyPairGenerator.generateKeyPair() + } +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt index f7d09e61..cfdc8f88 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt @@ -22,4 +22,10 @@ import eu.europa.ec.eudi.openid4vp.ResolvedRequestObject class OpenId4VpRequest( val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, val sessionTranscript: SessionTranscriptBytes, -) : Request \ No newline at end of file +) : Request + + +class OpenId4VpSdJwtRequest( + val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization +) : Request + diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt index 8a291ec6..c7807ed7 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt @@ -23,7 +23,20 @@ import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.util.Base64URL import eu.europa.ec.eudi.iso18013.transfer.TransferEvent import eu.europa.ec.eudi.iso18013.transfer.response.SessionTranscriptBytes -import eu.europa.ec.eudi.openid4vp.* +import eu.europa.ec.eudi.openid4vp.Consensus +import eu.europa.ec.eudi.openid4vp.DefaultHttpClientFactory +import eu.europa.ec.eudi.openid4vp.DispatchOutcome +import eu.europa.ec.eudi.openid4vp.JarmConfiguration +import eu.europa.ec.eudi.openid4vp.JwkSetSource +import eu.europa.ec.eudi.openid4vp.PreregisteredClient +import eu.europa.ec.eudi.openid4vp.Resolution +import eu.europa.ec.eudi.openid4vp.ResolvedRequestObject +import eu.europa.ec.eudi.openid4vp.ResponseMode +import eu.europa.ec.eudi.openid4vp.SiopOpenId4VPConfig +import eu.europa.ec.eudi.openid4vp.SiopOpenId4Vp +import eu.europa.ec.eudi.openid4vp.SupportedClientIdScheme +import eu.europa.ec.eudi.openid4vp.VpToken +import eu.europa.ec.eudi.openid4vp.asException import eu.europa.ec.eudi.prex.DescriptorMap import eu.europa.ec.eudi.prex.Id import eu.europa.ec.eudi.prex.JsonPath @@ -34,10 +47,11 @@ import eu.europa.ec.eudi.wallet.logging.Logger import eu.europa.ec.eudi.wallet.logging.d import eu.europa.ec.eudi.wallet.logging.e import eu.europa.ec.eudi.wallet.logging.i +import eu.europa.ec.eudi.wallet.transfer.openid4vp.responseGenerator.OpenId4VpResponseGeneratorDelegator import eu.europa.ec.eudi.wallet.util.CBOR import eu.europa.ec.eudi.wallet.util.wrappedWithContentNegotiation import eu.europa.ec.eudi.wallet.util.wrappedWithLogging -import io.ktor.client.* +import io.ktor.client.HttpClient import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -46,7 +60,8 @@ import org.bouncycastle.util.encoders.Hex import java.net.URI import java.net.URLDecoder import java.nio.charset.StandardCharsets -import java.util.* +import java.util.Base64 +import java.util.UUID import java.util.concurrent.Executor /** @@ -127,7 +142,7 @@ import java.util.concurrent.Executor class OpenId4vpManager( context: Context, openId4VpConfig: OpenId4VpConfig, - val responseGenerator: OpenId4VpCBORResponseGeneratorImpl, + val responseGenerator: OpenId4VpResponseGeneratorDelegator, ) : TransferEvent.Listenable { var logger: Logger? = null @@ -146,7 +161,8 @@ class OpenId4vpManager( transferEventListeners.onTransferEvent(result) } } - private val siopOpenId4Vp = SiopOpenId4Vp(openId4VpConfig.toSiopOpenId4VPConfig(), ktorHttpClientFactory) + private val siopOpenId4Vp = + SiopOpenId4Vp(openId4VpConfig.toSiopOpenId4VPConfig(), ktorHttpClientFactory) private var resolvedRequestObject: ResolvedRequestObject? = null private var mdocGeneratedNonce: String? = null @@ -192,10 +208,26 @@ class OpenId4vpManager( when (requestObject) { is ResolvedRequestObject.OpenId4VPAuthorization -> { logger?.d(TAG, "OpenId4VPAuthorization Request received") - val request = OpenId4VpRequest( - requestObject, - requestObject.toSessionTranscript() - ) + + val format = + requestObject.presentationDefinition.inputDescriptors.first().format?.jsonObject()?.keys?.first() //TODO Format Type nutzen + val request = when (format) { + "mso_mdoc" -> { + OpenId4VpRequest( + requestObject, + requestObject.toSessionTranscript() + ) + } + + "vc_sd_jwt" -> { + OpenId4VpSdJwtRequest(requestObject) + } + + else -> { + throw NotImplementedError(message = "Not supported: ${format}") + } + } + onResultUnderExecutor( TransferEvent.RequestReceived( responseGenerator.parseRequest(request), @@ -239,42 +271,28 @@ class OpenId4vpManager( */ fun sendResponse(deviceResponse: ByteArray) { - logger?.d(TAG, "Device Response to send (hex): ${Hex.toHexString(deviceResponse)}") - logger?.d(TAG, "Device Response to send (cbor): ${CBOR.cborPrettyPrint(deviceResponse)}") +// logger?.d(TAG, "Device Response to send (hex): ${Hex.toHexString(deviceResponse)}") +// logger?.d(TAG, "Device Response to send (cbor): ${CBOR.cborPrettyPrint(deviceResponse)}") ioScope.launch { resolvedRequestObject?.let { resolvedRequestObject -> when (resolvedRequestObject) { is ResolvedRequestObject.OpenId4VPAuthorization -> { - val vpToken = - Base64.getUrlEncoder().withoutPadding() - .encodeToString(deviceResponse) - logger?.d(TAG, "VpToken: $vpToken") - - val presentationDefinition = - (resolvedRequestObject).presentationDefinition - val consensus = Consensus.PositiveConsensus.VPTokenConsensus( - VpToken.MsoMdoc( - vpToken, - Base64URL.encode(mdocGeneratedNonce), - ), - presentationSubmission = PresentationSubmission( - id = Id(UUID.randomUUID().toString()), - definitionId = presentationDefinition.id, - presentationDefinition.inputDescriptors.map { inputDescriptor -> - DescriptorMap( - inputDescriptor.id, - "mso_mdoc", - path = JsonPath.jsonPath("$")!! - ) - } - ) - ) + val vpTokenConsensus = when (responseGenerator.formatState) { + OpenId4VpResponseGeneratorDelegator.FormatState.Cbor -> { + mDocVPTokenConsensus(deviceResponse, resolvedRequestObject) + } + + OpenId4VpResponseGeneratorDelegator.FormatState.SdJwt -> { + sdJwtVPTokenConsensus(deviceResponse, resolvedRequestObject) + } + } + runCatching { siopOpenId4Vp.dispatch( resolvedRequestObject, - consensus + vpTokenConsensus ) }.onSuccess { dispatchOutcome -> when (dispatchOutcome) { @@ -315,6 +333,63 @@ class OpenId4vpManager( } } + private fun mDocVPTokenConsensus( + deviceResponse: ByteArray, + resolvedRequestObject: ResolvedRequestObject.OpenId4VPAuthorization + ): Consensus.PositiveConsensus.VPTokenConsensus { + val vpToken = + Base64.getUrlEncoder().withoutPadding() + .encodeToString(deviceResponse) + logger?.d(TAG, "VpToken: $vpToken") + + val presentationDefinition = resolvedRequestObject.presentationDefinition + return Consensus.PositiveConsensus.VPTokenConsensus( + VpToken.MsoMdoc( + vpToken, + Base64URL.encode(mdocGeneratedNonce), + ), + presentationSubmission = PresentationSubmission( + id = Id(UUID.randomUUID().toString()), + definitionId = presentationDefinition.id, + presentationDefinition.inputDescriptors.map { inputDescriptor -> + DescriptorMap( + inputDescriptor.id, + "mso_mdoc", + path = JsonPath.jsonPath("$")!! + ) + } + ) + ) + } + + private fun sdJwtVPTokenConsensus( + deviceResponse: ByteArray, + resolvedRequestObject: ResolvedRequestObject.OpenId4VPAuthorization + ): Consensus.PositiveConsensus.VPTokenConsensus { + val vpToken = + Base64.getUrlEncoder().withoutPadding() + .encodeToString(deviceResponse) + logger?.d(TAG, "VpToken: $vpToken") + + val presentationDefinition = resolvedRequestObject.presentationDefinition + return Consensus.PositiveConsensus.VPTokenConsensus( + VpToken.Generic( + vpToken, + ), + presentationSubmission = PresentationSubmission( + id = Id(UUID.randomUUID().toString()), + definitionId = presentationDefinition.id, + presentationDefinition.inputDescriptors.map { inputDescriptor -> + DescriptorMap( + inputDescriptor.id, + "vc_sd_jwt", + path = JsonPath.jsonPath("$")!! + ) + } + ) + ) + } + /** * Closes the OpenId4VpManager */ @@ -363,7 +438,7 @@ class OpenId4vpManager( val responseUri = (this.responseMode as ResponseMode.DirectPostJwt?)?.responseURI?.toString() ?: "" - val nonce = this.nonce + val nonce = this.nonce //TODO MAYBE THIS NONCE val mdocGeneratedNonce = Openid4VpUtils.generateMdocGeneratedNonce().also { mdocGeneratedNonce = it } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpCBORResponseGeneratorImpl.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt similarity index 95% rename from wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpCBORResponseGeneratorImpl.kt rename to wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt index dff9f3c7..6c15c74e 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpCBORResponseGeneratorImpl.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package eu.europa.ec.eudi.wallet.transfer.openid4vp +package eu.europa.ec.eudi.wallet.transfer.openid4vp.responseGenerator import android.content.Context import com.android.identity.android.securearea.AndroidKeystoreSecureArea @@ -40,6 +40,8 @@ import eu.europa.ec.eudi.wallet.internal.Openid4VpX509CertificateTrust import eu.europa.ec.eudi.wallet.logging.Logger import eu.europa.ec.eudi.wallet.logging.e import eu.europa.ec.eudi.wallet.logging.i +import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpCBORResponse +import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpRequest private const val TAG = "OpenId4VpCBORResponseGe" @@ -73,21 +75,8 @@ class OpenId4VpCBORResponseGeneratorImpl( this.readerTrustStore = readerTrustStore } - /** - * Set a trust store so that reader authentication can be performed. - * - * If it is not provided, reader authentication will not be performed. - * - * @param readerTrustStore a trust store for reader authentication, e.g. DefaultReaderTrustStore - */ - fun readerTrustStore(readerTrustStore: ReaderTrustStore) = apply { - openid4VpX509CertificateTrust.setReaderTrustStore(readerTrustStore) - this.readerTrustStore = readerTrustStore - } - internal fun getOpenid4VpX509CertificateTrust() = openid4VpX509CertificateTrust - private val secureAreaRepository: SecureAreaRepository by lazy { SecureAreaRepository().apply { addImplementation(secureArea) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt new file mode 100644 index 00000000..bed5b0f3 --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt @@ -0,0 +1,112 @@ +package eu.europa.ec.eudi.wallet.transfer.openid4vp.responseGenerator + +import android.content.Context +import com.android.identity.android.securearea.AndroidKeystoreSecureArea +import com.android.identity.android.storage.AndroidStorageEngine +import com.android.identity.storage.StorageEngine +import eu.europa.ec.eudi.iso18013.transfer.DisclosedDocuments +import eu.europa.ec.eudi.iso18013.transfer.DocumentsResolver +import eu.europa.ec.eudi.iso18013.transfer.RequestedDocumentData +import eu.europa.ec.eudi.iso18013.transfer.ResponseResult +import eu.europa.ec.eudi.iso18013.transfer.readerauth.ReaderTrustStore +import eu.europa.ec.eudi.iso18013.transfer.response.Request +import eu.europa.ec.eudi.wallet.logging.Logger +import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpRequest +import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpSdJwtRequest + +class OpenId4VpResponseGeneratorDelegator( + private val mDocGenerator: OpenId4VpCBORResponseGeneratorImpl, + private val sdJwtGenerator: OpenId4VpSdJwtResponseGeneratorImpl +) { + enum class FormatState { + Cbor, + SdJwt + } + + var formatState: FormatState = FormatState.Cbor + private set + + fun createResponse(disclosedDocuments: DisclosedDocuments): ResponseResult { + return when (formatState) { + FormatState.Cbor -> mDocGenerator.createResponse(disclosedDocuments) + FormatState.SdJwt -> sdJwtGenerator.createResponse(disclosedDocuments) + } + } + + fun setReaderTrustStore(readerTrustStore: ReaderTrustStore) { + sdJwtGenerator.setReaderTrustStore(readerTrustStore) + mDocGenerator.setReaderTrustStore(readerTrustStore) + } + + internal fun getOpenid4VpX509CertificateTrust() = when (formatState) { + FormatState.Cbor -> mDocGenerator.getOpenid4VpX509CertificateTrust() + FormatState.SdJwt -> sdJwtGenerator.getOpenid4VpX509CertificateTrust() + } + + + fun parseRequest(request: Request): RequestedDocumentData { + return when (request) { + is OpenId4VpRequest -> { + formatState = FormatState.Cbor + mDocGenerator.parseRequest(request) + } + + is OpenId4VpSdJwtRequest -> { + formatState = FormatState.SdJwt + sdJwtGenerator.parseRequest(request) + } + + else -> { + throw NotImplementedError(message = "Not supported: ${request::class.simpleName}") + } + } + } + + class Builder(context: Context) { + private val _context = context.applicationContext + var documentsResolver: DocumentsResolver? = null + var readerTrustStore: ReaderTrustStore? = null + var logger: Logger? = null + + /** + * Reader trust store that will be used to validate the certificate chain of the mdoc verifier + * + * @param readerTrustStore + */ + fun readerTrustStore(readerTrustStore: ReaderTrustStore) = + apply { this.readerTrustStore = readerTrustStore } + + fun build(): OpenId4VpResponseGeneratorDelegator { + return documentsResolver?.let { documentsResolver -> + val openId4VpCBORResponseGeneratorImpl = OpenId4VpCBORResponseGeneratorImpl( + documentsResolver, + storageEngine, + androidSecureArea, + logger + ).apply { + readerTrustStore?.let { setReaderTrustStore(it) } + } + + val openId4VpSdJwtResponseGeneratorImpl = OpenId4VpSdJwtResponseGeneratorImpl( + documentsResolver + ).apply { + readerTrustStore?.let { setReaderTrustStore(it) } + } + + OpenId4VpResponseGeneratorDelegator( + openId4VpCBORResponseGeneratorImpl, + openId4VpSdJwtResponseGeneratorImpl + ).apply { + readerTrustStore?.let { setReaderTrustStore(it) } + } + } ?: throw IllegalArgumentException("documentResolver not set") + } + + private val storageEngine: StorageEngine + get() = AndroidStorageEngine.Builder(_context, _context.noBackupFilesDir) + .setUseEncryption(true) + .build() + private val androidSecureArea: AndroidKeystoreSecureArea + get() = AndroidKeystoreSecureArea(_context, storageEngine) + } +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt new file mode 100644 index 00000000..96ed316a --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt @@ -0,0 +1,192 @@ +package eu.europa.ec.eudi.wallet.transfer.openid4vp.responseGenerator + +import android.os.Build +import android.security.keystore.KeyProperties +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSHeader +import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.jca.JCAContext +import com.nimbusds.jose.jwk.AsymmetricJWK +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.util.Base64URL +import eu.europa.ec.eudi.iso18013.transfer.DisclosedDocuments +import eu.europa.ec.eudi.iso18013.transfer.DocItem +import eu.europa.ec.eudi.iso18013.transfer.DocRequest +import eu.europa.ec.eudi.iso18013.transfer.DocumentsResolver +import eu.europa.ec.eudi.iso18013.transfer.ReaderAuth +import eu.europa.ec.eudi.iso18013.transfer.RequestDocument +import eu.europa.ec.eudi.iso18013.transfer.RequestedDocumentData +import eu.europa.ec.eudi.iso18013.transfer.ResponseResult +import eu.europa.ec.eudi.iso18013.transfer.readerauth.ReaderTrustStore +import eu.europa.ec.eudi.iso18013.transfer.response.DeviceResponse +import eu.europa.ec.eudi.iso18013.transfer.response.ResponseGenerator +import eu.europa.ec.eudi.openid4vp.legalName +import eu.europa.ec.eudi.sdjwt.HashAlgorithm +import eu.europa.ec.eudi.sdjwt.JsonPointer +import eu.europa.ec.eudi.sdjwt.JwtAndClaims +import eu.europa.ec.eudi.sdjwt.KeyBindingSigner +import eu.europa.ec.eudi.sdjwt.SdJwt +import eu.europa.ec.eudi.sdjwt.SdJwtVerifier +import eu.europa.ec.eudi.sdjwt.asJwtVerifier +import eu.europa.ec.eudi.sdjwt.present +import eu.europa.ec.eudi.sdjwt.serializeWithKeyBinding +import eu.europa.ec.eudi.wallet.internal.Openid4VpX509CertificateTrust +import eu.europa.ec.eudi.wallet.issue.openid4vci.DocumentManagerSdJwt +import eu.europa.ec.eudi.wallet.issue.openid4vci.pem +import eu.europa.ec.eudi.wallet.keystore.DEV_KEY_ALIAS +import eu.europa.ec.eudi.wallet.keystore.KeyGenerator +import eu.europa.ec.eudi.wallet.keystore.KeyGeneratorImpl +import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpSdJwtRequest +import kotlinx.coroutines.runBlocking +import org.json.JSONObject +import java.io.ByteArrayInputStream +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.Base64 + +class OpenId4VpSdJwtResponseGeneratorImpl( + private val documentsResolver: DocumentsResolver, +) : ResponseGenerator() { + private var readerTrustStore: ReaderTrustStore? = null + private val openid4VpX509CertificateTrust = Openid4VpX509CertificateTrust(readerTrustStore) + + override fun createResponse(disclosedDocuments: DisclosedDocuments) = runBlocking { + val disclosedDocument = disclosedDocuments.documents.first() + + val credentials = DocumentManagerSdJwt.getDocumentById(disclosedDocument.documentId)?.data + ?: throw IllegalArgumentException() + + val sdJwt = getSdJwtFromCredentials(credentials) + + val jsonPointer = disclosedDocument.docRequest.requestItems.mapNotNull { item -> + JsonPointer.parse(item.elementIdentifier) + }.toSet() + + val presentationSdJwt = sdJwt.present(jsonPointer) + + val key = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + KeyGeneratorImpl.getSigningKey(KeyGenerator.SigningKeyConfig(KeyProperties.AUTH_DEVICE_CREDENTIAL, 60)) + } else { + throw Exception() + } + + val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + val certificate = + certificateFactory.generateCertificate(ByteArrayInputStream(key.certificate.encoded)) as X509Certificate + val ecKey = ECKey.parse(certificate) + + val string = presentationSdJwt!!.serializeWithKeyBinding( + jwtSerializer = { it.first }, + hashAlgorithm = HashAlgorithm.SHA_256, + keyBindingSigner = object : KeyBindingSigner { + override val signAlgorithm: JWSAlgorithm = JWSAlgorithm.ES256 + override val publicKey: AsymmetricJWK = ecKey.toPublicJWK() + override fun getJCAContext(): JCAContext = JCAContext() + @Throws(java.security.SignatureException::class) + override fun sign(p0: JWSHeader?, p1: ByteArray?): Base64URL = + Base64URL(KeyGeneratorImpl.sign(key.privateKey, p1 ?: ByteArray(0))) + }, + claimSetBuilderAction = { } + ) + + return@runBlocking ResponseResult.Success(DeviceResponse(string.toByteArray())) + } + + override fun setReaderTrustStore(readerTrustStore: ReaderTrustStore) = apply { + openid4VpX509CertificateTrust.setReaderTrustStore(readerTrustStore) + this.readerTrustStore = readerTrustStore + } + + internal fun getOpenid4VpX509CertificateTrust() = openid4VpX509CertificateTrust + + override fun parseRequest(request: OpenId4VpSdJwtRequest): RequestedDocumentData { + val inputDescriptors = + request.openId4VPAuthorization.presentationDefinition.inputDescriptors + .filter { inputDescriptor -> + inputDescriptor.format?.json?.contains("vc_sd_jwt") == true + } + + if (inputDescriptors.isEmpty()) { + throw IllegalArgumentException() + } + + val namespace = "eu.europa.ec.eudi.pid.1" + + val requestedFields = inputDescriptors.associate { inputDescriptor -> + inputDescriptor.id.value.trim() to inputDescriptor.constraints.fields() + .map { fieldConstraint -> + val elementIdentifier = fieldConstraint.paths.first().value + .replace(".", "/") + .drop(1) + + namespace to elementIdentifier + }.groupBy({ it.first }, { it.second }) + .mapValues { (_, values) -> values.toList() } + .toMap() + } + + val readerAuth = openid4VpX509CertificateTrust.getTrustResult()?.let { (chain, isTrusted) -> + ReaderAuth( + byteArrayOf(0), + true, /* It is always true as siop-openid4vp library validates it internally and returns a fail status */ + chain, + isTrusted, + request.openId4VPAuthorization.client.legalName() ?: "", + ) + } + + return createRequestedDocumentData(requestedFields, readerAuth) + } + + private fun createRequestedDocumentData( + requestedFields: Map>>, + readerAuth: ReaderAuth?, + ): RequestedDocumentData { + val requestedDocuments = mutableListOf() + requestedFields.forEach { document -> + // create doc item + val docItems = mutableListOf() + document.value.forEach { (namespace, elementIds) -> + elementIds.forEach { elementId -> + docItems.add(DocItem(namespace, elementId)) + } + } + val docType = document.key + + requestedDocuments.addAll( + documentsResolver.resolveDocuments( + DocRequest( + docType, + docItems, + readerAuth + ) + ) + ) + } + return RequestedDocumentData(requestedDocuments) + } + + private suspend fun getSdJwtFromCredentials(credentials: String): SdJwt.Issuance { + val headerString = credentials.split(".").first() + val headerJson = JSONObject(String(Base64.getUrlDecoder().decode(headerString))) + val keyString = headerJson.getJSONArray("x5c").getString(0).replace("\n", "") + println(keyString) + + val pemString = "-----BEGIN CERTIFICATE-----\n" + + "${keyString}\n" + + "-----END CERTIFICATE-----" + + val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + val certificate = + certificateFactory.generateCertificate(ByteArrayInputStream(pemString.toByteArray())) as X509Certificate + + val ecKey = ECKey.parse(certificate) + val jwtSignatureVerifier = ECDSAVerifier(ecKey).asJwtVerifier() + + return SdJwtVerifier.verifyIssuance( + jwtSignatureVerifier, + credentials + ).getOrThrow() + } +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/EncryptedSharedPrefsUtil.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/EncryptedSharedPrefsUtil.kt new file mode 100644 index 00000000..19d0625f --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/EncryptedSharedPrefsUtil.kt @@ -0,0 +1,39 @@ +package eu.europa.ec.eudi.wallet.util + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import javax.crypto.AEADBadTagException + +@Throws(java.security.GeneralSecurityException::class, java.io.IOException::class) +fun getEncryptedSharedPreferences(context: Context, name: String): SharedPreferences { + val masterKey: MasterKey = MasterKey + .Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + + return try { + createEncryptedSharedPreferences(context, masterKey, name) + } catch (e: AEADBadTagException) { + clearEncryptedSharedPreferences(context, name) + createEncryptedSharedPreferences(context, masterKey, name) + } +} + +@Throws(java.security.GeneralSecurityException::class, java.io.IOException::class) +private fun createEncryptedSharedPreferences( + context: Context, + masterKey: MasterKey, + name: String, +) = EncryptedSharedPreferences.create( + context, + name, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM +) + +private fun clearEncryptedSharedPreferences(context: Context, name: String) { + context.getSharedPreferences(name, Context.MODE_PRIVATE).edit().clear().apply() +} \ No newline at end of file diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt deleted file mode 100644 index c696f900..00000000 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/IssuerAuthorizationTest.kt +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright (c) 2024 European Commission - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package eu.europa.ec.eudi.wallet.issue.openid4vci - -import android.content.Context -import android.content.Intent -import android.net.Uri -import eu.europa.ec.eudi.openid4vci.* -import eu.europa.ec.eudi.wallet.logging.Logger -import io.mockk.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import kotlin.time.Duration.Companion.milliseconds - - -class IssuerAuthorizationTest { - - companion object { - lateinit var context: Context - lateinit var logger: Logger - lateinit var issuer: Issuer - - @BeforeAll - @JvmStatic - fun setup() { - - mockkStatic(Uri::class) - every { Uri.parse(any()) } returns mockk(relaxed = true) - - mockkConstructor(Intent::class) - every { anyConstructed().addFlags(any()) } returns mockk(relaxed = true) - - context = mockk(relaxed = true) - logger = mockk(relaxed = true) - } - } - - lateinit var preparedAuthorizationRequest: AuthorizationRequestPrepared - lateinit var authorizedRequest: AuthorizedRequest - - @BeforeEach - fun setupTest() { - preparedAuthorizationRequest = mockk(relaxed = true) - every { - preparedAuthorizationRequest.authorizationCodeURL - } returns HttpsUrl("https://test.com").getOrThrow() - - issuer = mockk(relaxed = true) - authorizedRequest = mockk(relaxed = true) - coEvery { - issuer.prepareAuthorizationRequest() - } returns Result.success(preparedAuthorizationRequest) - - coEvery { - with(issuer) { - preparedAuthorizationRequest.authorizeWithAuthorizationCode(any(), any()) - } - } returns Result.success(authorizedRequest) - - coEvery { - issuer.authorizeWithPreAuthorizationCode(any()) - } returns Result.success(authorizedRequest) - } - - @Test - fun `authorize method when no preAuthorizedCode in offer and txCode is null calls openBrowserForAuthorization`() { - every { issuer.credentialOffer } returns mockk(relaxed = true) { - every { grants } returns mockk(relaxed = true) { - every { preAuthorizedCode() } returns null - } - } - val issuerAuthorization = spyk(IssuerAuthorization(context, logger)) - runTest { - launch { - issuerAuthorization.authorize(issuer, null) - } - launch { - delay(500.milliseconds) - issuerAuthorization.close() - } - } - coVerify(exactly = 1) { - issuer.prepareAuthorizationRequest() - issuerAuthorization.openBrowserForAuthorization(preparedAuthorizationRequest) - } - } - - @Test - fun `authorize method when preAuthorizedCode in offer and passing txCode does not call openBrowserForAuthorization but calls authorizeWithPreAuthorizationCode`() { - every { issuer.credentialOffer } returns mockk(relaxed = true) { - every { grants } returns mockk(relaxed = true) { - every { preAuthorizedCode() } returns mockk(relaxed = true) { - every { txCode } returns mockk(relaxed = true) { - every { length } returns 4 - every { inputMode } returns TxCodeInputMode.NUMERIC - } - } - } - } - val issuerAuthorization = spyk(IssuerAuthorization(context, logger)) - runTest { - launch { - issuerAuthorization.authorize(issuer, "1234") - } - launch { - delay(500.milliseconds) - issuerAuthorization.close() - } - } - coVerify(exactly = 0) { - issuer.prepareAuthorizationRequest() - issuerAuthorization.openBrowserForAuthorization(preparedAuthorizationRequest) - } - coVerify(exactly = 1) { - issuer.authorizeWithPreAuthorizationCode("1234") - } - } - - @Test - fun `resumeFromUri resumes with success when authorization code and server state are present`() { - - val issuerAuthorization = spyk(IssuerAuthorization(context, logger)) - val uri = mockk(relaxed = true) { - every { getQueryParameter("code") } returns "testCode" - every { getQueryParameter("state") } returns "testState" - } - var result: Result? = null - runTest { - launch { - result = issuerAuthorization.openBrowserForAuthorization(preparedAuthorizationRequest) - } - - launch { - delay(500.milliseconds) - issuerAuthorization.resumeFromUri(uri) - } - } - assertNotNull(result, "Result is not null") - assertTrue(result!!.isSuccess) - assertEquals("testCode", result!!.getOrNull()!!.authorizationCode) - assertEquals("testState", result!!.getOrNull()!!.serverState) - verify(exactly = 1) { - issuerAuthorization.resumeFromUri(uri) - } - } - - @Test - fun `resumeFromUri resumes with failure when authorization code is missing`() { - val issuerAuthorization: IssuerAuthorization = spyk(IssuerAuthorization(context, logger)) - val uri = mockk(relaxed = true) { - every { getQueryParameter("code") } returns null - every { getQueryParameter("state") } returns "testState" - } - var result: Result? = null - runTest { - launch { - result = issuerAuthorization.openBrowserForAuthorization(preparedAuthorizationRequest) - } - - launch { - delay(500.milliseconds) - issuerAuthorization.resumeFromUri(uri) - } - } - assertNotNull(result, "Result is not null") - assertTrue(result!!.isFailure) - verify(exactly = 1) { - issuerAuthorization.resumeFromUri(uri) - } - } - - @Test - fun `resumeFromUri resumes with failure when server state is missing`() { - val issuerAuthorization: IssuerAuthorization = spyk(IssuerAuthorization(context, logger)) - val uri = mockk { - every { getQueryParameter("code") } returns "testCode" - every { getQueryParameter("state") } returns null - } - var result: Result? = null - runTest { - launch(Dispatchers.Default) { - result = issuerAuthorization.openBrowserForAuthorization(preparedAuthorizationRequest) - } - - - launch(Dispatchers.Default) { - delay(500.milliseconds) - issuerAuthorization.resumeFromUri(uri) - } - } - assertNotNull(result, "Result is not null") - assertTrue(result!!.isFailure, "Result failed") - verify(exactly = 1) { - issuerAuthorization.resumeFromUri(uri) - } - } -} \ No newline at end of file From f3556641dbd65dce83ba1683eed2d73bb4bbee35 Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Wed, 14 Aug 2024 14:22:24 +0200 Subject: [PATCH 2/8] Fix requested changes --- .../issue/openid4vci/ProcessResponse.kt | 15 +------- .../OpenId4VpSdJwtResponseGeneratorImpl.kt | 38 ++++++++----------- .../europa/ec/eudi/wallet/util/SdJwtUtil.kt | 23 +++++++++++ 3 files changed, 39 insertions(+), 37 deletions(-) create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt index 806bbda8..40edd708 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt @@ -97,20 +97,7 @@ internal class ProcessResponse( is SubmissionOutcome.Success -> when (val credential = outcome.credentials[0]) { is IssuedCredential.Issued -> try { if (isSdJwt(credential.credential)) { - val headerString = credential.credential.split(".").first() - val headerJson = - JSONObject(String(Base64.getUrlDecoder().decode(headerString))) - val keyString = - headerJson.getJSONArray("x5c").getString(0).replace("\n", "") - - val pemKey = "-----BEGIN CERTIFICATE-----\n" + - "${keyString}\n" + - "-----END CERTIFICATE-----" - - val certificateFactory: CertificateFactory = - CertificateFactory.getInstance("X.509") - val certificate = - certificateFactory.generateCertificate(ByteArrayInputStream(pemKey.toByteArray())) as X509Certificate + val certificate = parseCertificateFromSdJwt(credential.credential) val ecKey = ECKey.parse(certificate) val jwtSignatureVerifier = ECDSAVerifier(ecKey).asJwtVerifier() diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt index 96ed316a..c9b51577 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt @@ -2,9 +2,9 @@ package eu.europa.ec.eudi.wallet.transfer.openid4vp.responseGenerator import android.os.Build import android.security.keystore.KeyProperties +import android.util.AndroidException import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader -import com.nimbusds.jose.crypto.ECDSASigner import com.nimbusds.jose.crypto.ECDSAVerifier import com.nimbusds.jose.jca.JCAContext import com.nimbusds.jose.jwk.AsymmetricJWK @@ -33,17 +33,14 @@ import eu.europa.ec.eudi.sdjwt.present import eu.europa.ec.eudi.sdjwt.serializeWithKeyBinding import eu.europa.ec.eudi.wallet.internal.Openid4VpX509CertificateTrust import eu.europa.ec.eudi.wallet.issue.openid4vci.DocumentManagerSdJwt -import eu.europa.ec.eudi.wallet.issue.openid4vci.pem -import eu.europa.ec.eudi.wallet.keystore.DEV_KEY_ALIAS import eu.europa.ec.eudi.wallet.keystore.KeyGenerator import eu.europa.ec.eudi.wallet.keystore.KeyGeneratorImpl import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpSdJwtRequest +import eu.europa.ec.eudi.wallet.util.parseCertificateFromSdJwt import kotlinx.coroutines.runBlocking -import org.json.JSONObject import java.io.ByteArrayInputStream import java.security.cert.CertificateFactory import java.security.cert.X509Certificate -import java.util.Base64 class OpenId4VpSdJwtResponseGeneratorImpl( private val documentsResolver: DocumentsResolver, @@ -51,11 +48,13 @@ class OpenId4VpSdJwtResponseGeneratorImpl( private var readerTrustStore: ReaderTrustStore? = null private val openid4VpX509CertificateTrust = Openid4VpX509CertificateTrust(readerTrustStore) + private val sdJwtNamespace = "eu.europa.ec.eudi.pid.1" + override fun createResponse(disclosedDocuments: DisclosedDocuments) = runBlocking { val disclosedDocument = disclosedDocuments.documents.first() val credentials = DocumentManagerSdJwt.getDocumentById(disclosedDocument.documentId)?.data - ?: throw IllegalArgumentException() + ?: throw IllegalArgumentException() val sdJwt = getSdJwtFromCredentials(credentials) @@ -66,9 +65,14 @@ class OpenId4VpSdJwtResponseGeneratorImpl( val presentationSdJwt = sdJwt.present(jsonPointer) val key = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - KeyGeneratorImpl.getSigningKey(KeyGenerator.SigningKeyConfig(KeyProperties.AUTH_DEVICE_CREDENTIAL, 60)) + KeyGeneratorImpl.getSigningKey( + KeyGenerator.SigningKeyConfig( + KeyProperties.AUTH_DEVICE_CREDENTIAL, + 60 + ) + ) } else { - throw Exception() + return@runBlocking ResponseResult.Failure(AndroidException("Build version to low.")) } val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") @@ -83,6 +87,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( override val signAlgorithm: JWSAlgorithm = JWSAlgorithm.ES256 override val publicKey: AsymmetricJWK = ecKey.toPublicJWK() override fun getJCAContext(): JCAContext = JCAContext() + @Throws(java.security.SignatureException::class) override fun sign(p0: JWSHeader?, p1: ByteArray?): Base64URL = Base64URL(KeyGeneratorImpl.sign(key.privateKey, p1 ?: ByteArray(0))) @@ -111,8 +116,6 @@ class OpenId4VpSdJwtResponseGeneratorImpl( throw IllegalArgumentException() } - val namespace = "eu.europa.ec.eudi.pid.1" - val requestedFields = inputDescriptors.associate { inputDescriptor -> inputDescriptor.id.value.trim() to inputDescriptor.constraints.fields() .map { fieldConstraint -> @@ -120,7 +123,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( .replace(".", "/") .drop(1) - namespace to elementIdentifier + sdJwtNamespace to elementIdentifier }.groupBy({ it.first }, { it.second }) .mapValues { (_, values) -> values.toList() } .toMap() @@ -168,18 +171,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( } private suspend fun getSdJwtFromCredentials(credentials: String): SdJwt.Issuance { - val headerString = credentials.split(".").first() - val headerJson = JSONObject(String(Base64.getUrlDecoder().decode(headerString))) - val keyString = headerJson.getJSONArray("x5c").getString(0).replace("\n", "") - println(keyString) - - val pemString = "-----BEGIN CERTIFICATE-----\n" + - "${keyString}\n" + - "-----END CERTIFICATE-----" - - val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") - val certificate = - certificateFactory.generateCertificate(ByteArrayInputStream(pemString.toByteArray())) as X509Certificate + val certificate = parseCertificateFromSdJwt(credentials) val ecKey = ECKey.parse(certificate) val jwtSignatureVerifier = ECDSAVerifier(ecKey).asJwtVerifier() diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt new file mode 100644 index 00000000..894b20f4 --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt @@ -0,0 +1,23 @@ +package eu.europa.ec.eudi.wallet.util + +import org.json.JSONObject +import java.io.ByteArrayInputStream +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.Base64 + +public fun parseCertificateFromSdJwt(credential: String): X509Certificate { + val headerString = credential.split(".").first() + val headerJson = + JSONObject(String(Base64.getUrlDecoder().decode(headerString))) + val keyString = + headerJson.getJSONArray("x5c").getString(0).replace("\n", "") + + val pemKey = "-----BEGIN CERTIFICATE-----\n" + + "${keyString}\n" + + "-----END CERTIFICATE-----" + + val certificateFactory: CertificateFactory = + CertificateFactory.getInstance("X.509") + return certificateFactory.generateCertificate(ByteArrayInputStream(pemKey.toByteArray())) as X509Certificate +} \ No newline at end of file From 500c785911fb6e41325793f37b98ec524a00a738 Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Wed, 14 Aug 2024 14:22:33 +0200 Subject: [PATCH 3/8] Fix delete sdJwt document from mdoc storage --- .../europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt index 40edd708..d642ebd7 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt @@ -30,6 +30,7 @@ import eu.europa.ec.eudi.wallet.issue.openid4vci.IssueEvent.Companion.documentFa import eu.europa.ec.eudi.wallet.logging.Logger import eu.europa.ec.eudi.wallet.logging.d import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4vpManager.Companion.TAG +import eu.europa.ec.eudi.wallet.util.parseCertificateFromSdJwt import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -112,6 +113,7 @@ internal class ProcessResponse( unsignedDocument.id, credential.credential ) + documentManager.deleteDocumentById(unsignedDocument.id) listener.invoke( IssueEvent.DocumentIssued( unsignedDocument.id, From 8f8f73e66609df151977b42d7591d481ac9aaabd Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Wed, 14 Aug 2024 14:46:38 +0200 Subject: [PATCH 4/8] Fix mdoc presentation --- .../eudi/wallet/issue/openid4vci/ProcessResponse.kt | 3 --- .../wallet/transfer/openid4vp/OpenId4vpManager.kt | 11 +++++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt index d642ebd7..c5faea07 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt @@ -40,10 +40,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.serialization.json.Json import org.bouncycastle.util.encoders.Hex import org.json.JSONObject -import java.io.ByteArrayInputStream import java.io.Closeable -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate import java.util.Base64 import kotlin.coroutines.resume diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt index c7807ed7..71da0e4d 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt @@ -435,10 +435,13 @@ class OpenId4vpManager( private fun ResolvedRequestObject.OpenId4VPAuthorization.toSessionTranscript(): SessionTranscriptBytes { val clientId = this.client.id - val responseUri = - (this.responseMode as ResponseMode.DirectPostJwt?)?.responseURI?.toString() - ?: "" - val nonce = this.nonce //TODO MAYBE THIS NONCE + val responseUri = when (this.responseMode) { + is ResponseMode.DirectPost -> (this.responseMode as ResponseMode.DirectPost?)?.responseURI?.toString() ?: "" + is ResponseMode.DirectPostJwt -> (this.responseMode as ResponseMode.DirectPostJwt?)?.responseURI?.toString() ?: "" + else -> "" + } + + val nonce = this.nonce val mdocGeneratedNonce = Openid4VpUtils.generateMdocGeneratedNonce().also { mdocGeneratedNonce = it } From df11201e96e5e2af6841704176bb9c83a37bd042 Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Wed, 14 Aug 2024 15:52:55 +0200 Subject: [PATCH 5/8] Add zkp --- gradle/libs.versions.toml | 10 + wallet-core/build.gradle | 11 +- .../eu/europa/ec/eudi/wallet/EudiWallet.kt | 57 +++--- .../issue/openid4vci/DocumentManagerSdJwt.kt | 1 - .../transfer/openid4vp/OpenId4VpRequest.kt | 15 +- .../transfer/openid4vp/OpenId4vpManager.kt | 185 ++++++++++-------- .../OpenId4VpCBORResponseGeneratorImpl.kt | 118 ++++++++--- .../OpenId4VpResponseGeneratorDelegator.kt | 26 +-- .../OpenId4VpSdJwtResponseGeneratorImpl.kt | 28 ++- .../ec/eudi/wallet/util/CertificateUtil.kt | 40 ++++ .../europa/ec/eudi/wallet/util/SdJwtUtil.kt | 23 --- .../ec/eudi/wallet/zkp/network/ZKPClient.kt | 102 ++++++++++ 12 files changed, 448 insertions(+), 168 deletions(-) create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt delete mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt create mode 100644 wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3729948c..3f2668dd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,11 +22,15 @@ junit = "4.13.2" junit-android = "1.1.5" kotlin = "1.8.10" ktor = "2.3.5" +#ktorClientSerialization = "2.3.6" mavenPublish = "0.25.3" mockito-android = "4.0.0" mockito-inline = "4.0.0" mockk = "1.13.5" nimbus-sdk = "11.8" +okhttp = "4.12.0" +retrofit = "2.11.0" +retrofit2KotlinxSerializationConverter = "1.0.0" securityCrypto = "1.1.0-alpha06" sonarqube = "4.4.1.3373" test-core = "1.4.0" @@ -34,6 +38,7 @@ test-rules = "1.4.0" test-runner = "1.4.0" upokecenter-cbor = "4.5.2" kotlin-coroutines-test = "1.3.1" +zkp = "java17-SNAPSHOT" [libraries] android-identity-credential = { module = "com.android.identity:identity-credential-android", version.ref = "identity-credential-android" } @@ -58,11 +63,15 @@ junit = { module = "junit:junit", version.ref = "junit" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } +#ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktorClientSerialization" } mockito-android = { module = "org.mockito:mockito-android", version.ref = "mockito-android" } mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-inline" } mockk = { module = "io.mockk:mockk", version.ref = "mockk" } nimbus-oauth2-oidc-sdk = { module = "com.nimbusds:oauth2-oidc-sdk", version.ref = "nimbus-sdk" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit2-kotlinx-serialization-converter = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit2KotlinxSerializationConverter" } security-crypto = { module = "androidx.security:security-crypto", version.ref = "securityCrypto" } test-core = { module = "androidx.test:core", version.ref = "test-core" } test-coreKtx = { module = "androidx.test:core-ktx", version.ref = "test-core" } @@ -70,6 +79,7 @@ test-rules = { module = "androidx.test:rules", version.ref = "test-rules" } test-runner = { module = "androidx.test:runner", version.ref = "test-runner" } kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines-test" } upokecenter-cbor = { module = "com.upokecenter:cbor", version.ref = "upokecenter-cbor" } +zkp = { module = "com.github.TICESoftware:ZKP", version.ref = "zkp" } [plugins] android-library = { id = "com.android.library", version.ref = "gradle-plugin" } diff --git a/wallet-core/build.gradle b/wallet-core/build.gradle index 1aa017bc..0757ee6a 100644 --- a/wallet-core/build.gradle +++ b/wallet-core/build.gradle @@ -149,6 +149,7 @@ dependencies { // Ktor Android Engine runtimeOnly libs.ktor.client.android +// implementation libs.ktor.client.serialization implementation libs.ktor.client.logging // Bouncy Castle @@ -158,10 +159,18 @@ dependencies { api libs.upokecenter.cbor api libs.cose.java - //sdjwt + // sdjwt api libs.eudi.lib.jvm.sdjwt.kt implementation libs.security.crypto + // zkp + api(libs.zkp) { + exclude group: "org.bouncycastle" + } + implementation(libs.retrofit2.kotlinx.serialization.converter) + implementation(libs.retrofit) + implementation(libs.okhttp) + testImplementation libs.junit testImplementation libs.junit.jupiter.params testImplementation libs.json diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt index 04aaf6bb..bd996596 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/EudiWallet.kt @@ -692,11 +692,12 @@ object EudiWallet : KeyGenerator by KeyGeneratorImpl { when (transferMode) { TransferMode.OPENID4VP -> openId4vpManager?.sendResponse( - when(val result = responseResult.response){ + when (val result = responseResult.response) { is OpenId4VpCBORResponse -> result.deviceResponseBytes is DeviceResponse -> result.deviceResponseBytes else -> throw Exception() - }) + } + ) TransferMode.ISO_18013_5, TransferMode.REST_API -> transferManager.sendResponse((responseResult.response as DeviceResponse).deviceResponseBytes) @@ -747,31 +748,35 @@ object EudiWallet : KeyGenerator by KeyGeneratorImpl { private val transferManagerDocumentsResolver: DocumentsResolver get() = DocumentsResolver { req -> - DocumentManagerSdJwt - .getAllDocuments() -// .filter { doc -> doc.vct == req.docType } - .map { doc -> - RequestDocument( - documentId = doc.id, - docType = doc.vct, - docName = doc.docName, - userAuthentication = doc.requiresUserAuth, - docRequest = req - ) - }.takeIf { it.isNotEmpty() }?.let { return@DocumentsResolver it } - - return@DocumentsResolver documentManager.getDocuments(Document.State.ISSUED) - .filterIsInstance() -// .filter { doc -> doc.docType == req.docType } - .map { doc -> - RequestDocument( - documentId = doc.id, - docType = doc.docType, - docName = doc.name, - userAuthentication = doc.requiresUserAuth, - docRequest = req - ) + return@DocumentsResolver when (OpenId4VpResponseGeneratorDelegator.formatState) { + is OpenId4VpResponseGeneratorDelegator.FormatState.SdJwt -> { + DocumentManagerSdJwt + .getAllDocuments() + .map { doc -> + RequestDocument( + documentId = doc.id, + docType = doc.vct, + docName = doc.docName, + userAuthentication = doc.requiresUserAuth, + docRequest = req + ) + } } + + is OpenId4VpResponseGeneratorDelegator.FormatState.Cbor -> { + documentManager.getDocuments(Document.State.ISSUED) + .filterIsInstance() + .map { doc -> + RequestDocument( + documentId = doc.id, + docType = doc.docType, + docName = doc.name, + userAuthentication = doc.requiresUserAuth, + docRequest = req + ) + } + } + } } private val deviceResponseGenerator: ResponseGenerator by lazy { diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt index b2d0078d..39ed3119 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DocumentManagerSdJwt.kt @@ -30,7 +30,6 @@ object DocumentManagerSdJwt { fun deleteAllDocuments() { dataStore.deleteAll() } - } data class SdJwtDocument( diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt index cfdc8f88..3d5d5db3 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt @@ -22,10 +22,23 @@ import eu.europa.ec.eudi.openid4vp.ResolvedRequestObject class OpenId4VpRequest( val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, val sessionTranscript: SessionTranscriptBytes, + val requestId: String? = null, ) : Request +//class OpenId4VpZkpRequest( +// val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, +// val sessionTranscript: SessionTranscriptBytes, +// val requestId: String, +//) : Request + class OpenId4VpSdJwtRequest( - val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization + val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, + val requestId: String? = null, ) : Request +//class OpenId4VpSdJwtZkpRequest( +// val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, +// val requestId: String, +//) : Request + diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt index 71da0e4d..d4a12518 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4vpManager.kt @@ -17,6 +17,8 @@ package eu.europa.ec.eudi.wallet.transfer.openid4vp import android.content.Context +import android.net.Uri +import androidx.core.net.toUri import com.nimbusds.jose.EncryptionMethod import com.nimbusds.jose.JWEAlgorithm import com.nimbusds.jose.JWSAlgorithm @@ -48,7 +50,6 @@ import eu.europa.ec.eudi.wallet.logging.d import eu.europa.ec.eudi.wallet.logging.e import eu.europa.ec.eudi.wallet.logging.i import eu.europa.ec.eudi.wallet.transfer.openid4vp.responseGenerator.OpenId4VpResponseGeneratorDelegator -import eu.europa.ec.eudi.wallet.util.CBOR import eu.europa.ec.eudi.wallet.util.wrappedWithContentNegotiation import eu.europa.ec.eudi.wallet.util.wrappedWithLogging import io.ktor.client.HttpClient @@ -194,68 +195,29 @@ class OpenId4vpManager( ioScope.launch { onResultUnderExecutor(TransferEvent.Connecting) runCatching { siopOpenId4Vp.resolveRequestUri(openid4VPURI) }.onSuccess { resolution -> - when (resolution) { - is Resolution.Invalid -> { - logger?.e(TAG, "Resolution.Invalid", resolution.error.asException()) - onResultUnderExecutor(TransferEvent.Error(resolution.error.asException())) - } - - is Resolution.Success -> { - logger?.d(TAG, "Resolution.Success") - resolution.requestObject - .also { resolvedRequestObject = it } - .let { requestObject -> - when (requestObject) { - is ResolvedRequestObject.OpenId4VPAuthorization -> { - logger?.d(TAG, "OpenId4VPAuthorization Request received") - - val format = - requestObject.presentationDefinition.inputDescriptors.first().format?.jsonObject()?.keys?.first() //TODO Format Type nutzen - val request = when (format) { - "mso_mdoc" -> { - OpenId4VpRequest( - requestObject, - requestObject.toSessionTranscript() - ) - } - - "vc_sd_jwt" -> { - OpenId4VpSdJwtRequest(requestObject) - } - - else -> { - throw NotImplementedError(message = "Not supported: ${format}") - } - } - - onResultUnderExecutor( - TransferEvent.RequestReceived( - responseGenerator.parseRequest(request), - request - ) - ) - } - - is ResolvedRequestObject.SiopAuthentication -> { - logger?.i(TAG, "SiopAuthentication Request received") - onResultUnderExecutor("SiopAuthentication request received, not supported yet.".err()) - } - - is ResolvedRequestObject.SiopOpenId4VPAuthentication -> { - logger?.i( - TAG, - "SiopOpenId4VPAuthentication Request received" - ) - onResultUnderExecutor("SiopAuthentication request received, not supported yet.".err()) - } + try { + when (resolution) { + is Resolution.Invalid -> { + logger?.e(TAG, "Resolution.Invalid", resolution.error.asException()) + onResultUnderExecutor(TransferEvent.Error(resolution.error.asException())) + } - else -> { - logger?.e(TAG, "Unknown request received") - onResultUnderExecutor("Unknown request received".err()) - } - } + is Resolution.Success -> { + logger?.d(TAG, "Resolution.Success") + val requestId = Uri.parse(openid4VPURI).run { + getQueryParameter(queryParameterNames.elementAt(1))?.toUri()?.pathSegments?.get( + 2 + ) } + + resolution.requestObject + .also { resolvedRequestObject = it } + .let { handleRequestObject(it, requestId) } + } } + } catch (e: Exception) { + logger?.e(TAG, "An error occurred resolving request uri: $openid4VPURI", e) + onResultUnderExecutor(TransferEvent.Error(e)) } }.onFailure { logger?.e(TAG, "An error occurred resolving request uri: $openid4VPURI", it) @@ -264,30 +226,91 @@ class OpenId4vpManager( } } + private fun handleRequestObject(requestObject: ResolvedRequestObject, requestId: String?) { + when (requestObject) { + is ResolvedRequestObject.OpenId4VPAuthorization -> { + logger?.d(TAG, "OpenId4VPAuthorization Request received") + + val format = + requestObject.presentationDefinition.inputDescriptors.first().format?.jsonObject()?.keys?.first() + val request = when (format) { + "mso_mdoc" -> { + OpenId4VpRequest( + requestObject, + requestObject.toSessionTranscript() + ) + } + + "mso_mdoc+zkp" -> { + OpenId4VpRequest( + requestObject, + requestObject.toSessionTranscript(), + requestId, + ) + } + + "vc+sd-jwt" -> { + OpenId4VpSdJwtRequest(requestObject) + } + + "vc+sd-jwt+zkp" -> { + OpenId4VpSdJwtRequest(requestObject, requestId) + } + + else -> { + throw NotImplementedError(message = "Not supported: ${format}") + } + } + + onResultUnderExecutor( + TransferEvent.RequestReceived( + responseGenerator.parseRequest(request), + request + ) + ) + } + + is ResolvedRequestObject.SiopAuthentication -> { + logger?.i(TAG, "SiopAuthentication Request received") + onResultUnderExecutor("SiopAuthentication request received, not supported yet.".err()) + } + + is ResolvedRequestObject.SiopOpenId4VPAuthentication -> { + logger?.i( + TAG, + "SiopOpenId4VPAuthentication Request received" + ) + onResultUnderExecutor("SiopAuthentication request received, not supported yet.".err()) + } + + else -> { + logger?.e(TAG, "Unknown request received") + onResultUnderExecutor("Unknown request received".err()) + } + } + } + /** * Sends a response to the verifier * * @param deviceResponse */ fun sendResponse(deviceResponse: ByteArray) { - -// logger?.d(TAG, "Device Response to send (hex): ${Hex.toHexString(deviceResponse)}") -// logger?.d(TAG, "Device Response to send (cbor): ${CBOR.cborPrettyPrint(deviceResponse)}") - ioScope.launch { resolvedRequestObject?.let { resolvedRequestObject -> when (resolvedRequestObject) { is ResolvedRequestObject.OpenId4VPAuthorization -> { - val vpTokenConsensus = when (responseGenerator.formatState) { - OpenId4VpResponseGeneratorDelegator.FormatState.Cbor -> { - mDocVPTokenConsensus(deviceResponse, resolvedRequestObject) - } + val vpTokenConsensus = + when (OpenId4VpResponseGeneratorDelegator.formatState) { + is OpenId4VpResponseGeneratorDelegator.FormatState.Cbor -> { + mDocVPTokenConsensus(deviceResponse, resolvedRequestObject) + } - OpenId4VpResponseGeneratorDelegator.FormatState.SdJwt -> { - sdJwtVPTokenConsensus(deviceResponse, resolvedRequestObject) + is OpenId4VpResponseGeneratorDelegator.FormatState.SdJwt -> { + sdJwtVPTokenConsensus(deviceResponse, resolvedRequestObject) + } } - } runCatching { siopOpenId4Vp.dispatch( @@ -301,10 +324,10 @@ class OpenId4vpManager( TAG, "VerifierResponse Accepted with redirectUri: $dispatchOutcome.redirectURI" ) - onResultUnderExecutor(TransferEvent.ResponseSent) dispatchOutcome.redirectURI?.let { onResultUnderExecutor(TransferEvent.Redirect(it)) } + onResultUnderExecutor(TransferEvent.ResponseSent) } is DispatchOutcome.VerifierResponse.Rejected -> { @@ -343,6 +366,7 @@ class OpenId4vpManager( logger?.d(TAG, "VpToken: $vpToken") val presentationDefinition = resolvedRequestObject.presentationDefinition + val isZkp = OpenId4VpResponseGeneratorDelegator.formatState.isZkp return Consensus.PositiveConsensus.VPTokenConsensus( VpToken.MsoMdoc( vpToken, @@ -354,7 +378,7 @@ class OpenId4vpManager( presentationDefinition.inputDescriptors.map { inputDescriptor -> DescriptorMap( inputDescriptor.id, - "mso_mdoc", + if (isZkp) "mso_mdoc+zkp" else "mso_mdoc", path = JsonPath.jsonPath("$")!! ) } @@ -366,12 +390,11 @@ class OpenId4vpManager( deviceResponse: ByteArray, resolvedRequestObject: ResolvedRequestObject.OpenId4VPAuthorization ): Consensus.PositiveConsensus.VPTokenConsensus { - val vpToken = - Base64.getUrlEncoder().withoutPadding() - .encodeToString(deviceResponse) + val vpToken = String(deviceResponse) logger?.d(TAG, "VpToken: $vpToken") val presentationDefinition = resolvedRequestObject.presentationDefinition + val isZkp = OpenId4VpResponseGeneratorDelegator.formatState.isZkp return Consensus.PositiveConsensus.VPTokenConsensus( VpToken.Generic( vpToken, @@ -382,7 +405,7 @@ class OpenId4vpManager( presentationDefinition.inputDescriptors.map { inputDescriptor -> DescriptorMap( inputDescriptor.id, - "vc_sd_jwt", + if (isZkp) "vc+sd-jwt+zkp" else "vc+sd-jwt", path = JsonPath.jsonPath("$")!! ) } @@ -436,8 +459,12 @@ class OpenId4vpManager( private fun ResolvedRequestObject.OpenId4VPAuthorization.toSessionTranscript(): SessionTranscriptBytes { val clientId = this.client.id val responseUri = when (this.responseMode) { - is ResponseMode.DirectPost -> (this.responseMode as ResponseMode.DirectPost?)?.responseURI?.toString() ?: "" - is ResponseMode.DirectPostJwt -> (this.responseMode as ResponseMode.DirectPostJwt?)?.responseURI?.toString() ?: "" + is ResponseMode.DirectPost -> (this.responseMode as ResponseMode.DirectPost?)?.responseURI?.toString() + ?: "" + + is ResponseMode.DirectPostJwt -> (this.responseMode as ResponseMode.DirectPostJwt?)?.responseURI?.toString() + ?: "" + else -> "" } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt index 6c15c74e..fcdec912 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt @@ -31,7 +31,15 @@ import com.android.identity.securearea.SecureAreaRepository import com.android.identity.storage.StorageEngine import com.android.identity.util.Constants import com.android.identity.util.Timestamp -import eu.europa.ec.eudi.iso18013.transfer.* +import eu.europa.ec.eudi.iso18013.transfer.DisclosedDocument +import eu.europa.ec.eudi.iso18013.transfer.DisclosedDocuments +import eu.europa.ec.eudi.iso18013.transfer.DocItem +import eu.europa.ec.eudi.iso18013.transfer.DocRequest +import eu.europa.ec.eudi.iso18013.transfer.DocumentsResolver +import eu.europa.ec.eudi.iso18013.transfer.ReaderAuth +import eu.europa.ec.eudi.iso18013.transfer.RequestDocument +import eu.europa.ec.eudi.iso18013.transfer.RequestedDocumentData +import eu.europa.ec.eudi.iso18013.transfer.ResponseResult import eu.europa.ec.eudi.iso18013.transfer.readerauth.ReaderTrustStore import eu.europa.ec.eudi.iso18013.transfer.response.ResponseGenerator import eu.europa.ec.eudi.iso18013.transfer.response.SessionTranscriptBytes @@ -42,6 +50,13 @@ import eu.europa.ec.eudi.wallet.logging.e import eu.europa.ec.eudi.wallet.logging.i import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpCBORResponse import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpRequest +import eu.europa.ec.eudi.wallet.util.ZKP_ISSUER_CERT +import eu.europa.ec.eudi.wallet.util.getECPublicKeyFromCert +import eu.europa.ec.eudi.wallet.zkp.network.ZKPClient +import kotlinx.coroutines.runBlocking +import software.tice.ZKPGenerator +import software.tice.ZKPProverMdoc +import java.util.Base64 private const val TAG = "OpenId4VpCBORResponseGe" @@ -63,6 +78,8 @@ class OpenId4VpCBORResponseGeneratorImpl( private val openid4VpX509CertificateTrust = Openid4VpX509CertificateTrust(readerTrustStore) private var sessionTranscript: SessionTranscriptBytes? = null + private var zkpRequestId: String? = null + /** * Set a trust store so that reader authentication can be performed. * @@ -88,12 +105,14 @@ class OpenId4VpCBORResponseGeneratorImpl( * @return [RequestedDocumentData] */ override fun parseRequest(request: OpenId4VpRequest): RequestedDocumentData { + zkpRequestId = request.requestId sessionTranscript = request.sessionTranscript + return createRequestedDocumentData( request.openId4VPAuthorization.presentationDefinition.inputDescriptors .mapNotNull { inputDescriptor -> inputDescriptor.format?.jsonObject() - ?.takeIf { it.containsKey("mso_mdoc") } // ignore formats other than "mso_mdoc" + ?.takeIf { it.containsKey("mso_mdoc") || it.containsKey("mso_mdoc+zkp")} // ignore formats other than "mso_mdoc" ?.run { inputDescriptor.id.value.trim() to inputDescriptor.constraints.fields() .mapNotNull { fieldConstraint -> @@ -139,36 +158,76 @@ class OpenId4VpCBORResponseGeneratorImpl( */ override fun createResponse( disclosedDocuments: DisclosedDocuments, - ): ResponseResult { + ) = runBlocking { try { val deviceResponse = DeviceResponseGenerator(Constants.DEVICE_RESPONSE_STATUS_OK) + val documentDocTypeToByteArrays = mutableListOf>() + disclosedDocuments.documents.forEach { responseDocument -> if (responseDocument.docType == "org.iso.18013.5.1.mDL" && responseDocument.selectedDocItems.filter { docItem -> docItem.elementIdentifier.startsWith("age_over_") && docItem.namespace == "org.iso.18013.5.1" }.size > 2) { - return ResponseResult.Failure(Exception("Device Response is not allowed to have more than to age_over_NN elements")) + return@runBlocking ResponseResult.Failure(Exception("Device Response is not allowed to have more than to age_over_NN elements")) + } + + getDocumentByteArray(responseDocument, sessionTranscript!!).let { + when (it) { + is DocumentByteArrayResponse.UserAuthRequired -> { + return@runBlocking ResponseResult.UserAuthRequired( + it.keyUnlockData.getCryptoObjectForSigning( + SecureArea.ALGORITHM_ES256 + ) + ) + } + + is DocumentByteArrayResponse.Success -> documentDocTypeToByteArrays.add( + responseDocument.docType to it.byteArray + ) + } + } + } + + if (zkpRequestId == null) { + documentDocTypeToByteArrays.forEach { + deviceResponse.addDocument(it.second) + } + } else { + val zkpKey = getECPublicKeyFromCert(ZKP_ISSUER_CERT) + val prover = ZKPProverMdoc(ZKPGenerator(zkpKey)) + val requestData = documentDocTypeToByteArrays.map { + it.first to prover.createChallengeRequest( + Base64.getEncoder().encodeToString(it.second) + ) } - val addResult = - addDocumentToResponse(deviceResponse, responseDocument, sessionTranscript!!) - if (addResult is AddDocumentToResponse.UserAuthRequired) - return ResponseResult.UserAuthRequired( - addResult.keyUnlockData.getCryptoObjectForSigning(SecureArea.ALGORITHM_ES256) + val challenges = ZKPClient().getChallenges( + zkpRequestId!!, + requestData, + ) + challenges?.forEach { + deviceResponse.addDocument( + Base64.getDecoder().decode(prover.answerChallenge(it.second, it.first)) ) + } } + sessionTranscript = null - return ResponseResult.Success(OpenId4VpCBORResponse(deviceResponse.generate())) + + return@runBlocking ResponseResult.Success(OpenId4VpCBORResponse(deviceResponse.generate())) } catch (e: Exception) { - return ResponseResult.Failure(e) + return@runBlocking ResponseResult.Failure(e) } } - @Throws(IllegalStateException::class) - private fun addDocumentToResponse( - responseGenerator: DeviceResponseGenerator, + + @Throws( + IllegalStateException::class, + SecureArea.KeyLockedException::class + ) + private fun getDocumentByteArray( disclosedDocument: DisclosedDocument, transcript: ByteArray, - ): AddDocumentToResponse { + ): DocumentByteArrayResponse { val dataElements = disclosedDocument.selectedDocItems.map { CredentialRequest.DataElement(it.namespace, it.elementIdentifier, false) } @@ -194,13 +253,12 @@ class OpenId4VpCBORResponseGeneratorImpl( keyUnlockData, SecureArea.ALGORITHM_ES256 ) - val data = generator.generate() - responseGenerator.addDocument(data) + + return DocumentByteArrayResponse.Success(generator.generate()) } catch (lockedException: SecureArea.KeyLockedException) { logger?.e(TAG, "error", lockedException) - return AddDocumentToResponse.UserAuthRequired(keyUnlockData) + return DocumentByteArrayResponse.UserAuthRequired(keyUnlockData) } - return AddDocumentToResponse.Success } private fun createRequestedDocumentData( @@ -266,9 +324,25 @@ class OpenId4VpCBORResponseGeneratorImpl( get() = AndroidKeystoreSecureArea(_context, storageEngine) } - private sealed interface AddDocumentToResponse { - object Success : AddDocumentToResponse + private sealed interface DocumentByteArrayResponse { + data class Success( + val byteArray: ByteArray, + ) : DocumentByteArrayResponse { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Success + + return byteArray.contentEquals(other.byteArray) + } + + override fun hashCode(): Int { + return byteArray.contentHashCode() + } + } + data class UserAuthRequired(val keyUnlockData: AndroidKeystoreSecureArea.KeyUnlockData) : - AddDocumentToResponse + DocumentByteArrayResponse } } \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt index bed5b0f3..9d904a65 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpResponseGeneratorDelegator.kt @@ -14,22 +14,25 @@ import eu.europa.ec.eudi.wallet.logging.Logger import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpRequest import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpSdJwtRequest + class OpenId4VpResponseGeneratorDelegator( private val mDocGenerator: OpenId4VpCBORResponseGeneratorImpl, private val sdJwtGenerator: OpenId4VpSdJwtResponseGeneratorImpl ) { - enum class FormatState { - Cbor, - SdJwt + sealed class FormatState(open val isZkp: Boolean) { + data class Cbor(override val isZkp: Boolean) : FormatState(isZkp) + data class SdJwt(override val isZkp: Boolean) : FormatState(isZkp) } - var formatState: FormatState = FormatState.Cbor - private set + companion object { + public var formatState: FormatState = FormatState.Cbor(false) + private set + } fun createResponse(disclosedDocuments: DisclosedDocuments): ResponseResult { return when (formatState) { - FormatState.Cbor -> mDocGenerator.createResponse(disclosedDocuments) - FormatState.SdJwt -> sdJwtGenerator.createResponse(disclosedDocuments) + is FormatState.Cbor -> mDocGenerator.createResponse(disclosedDocuments) + is FormatState.SdJwt -> sdJwtGenerator.createResponse(disclosedDocuments) } } @@ -39,20 +42,19 @@ class OpenId4VpResponseGeneratorDelegator( } internal fun getOpenid4VpX509CertificateTrust() = when (formatState) { - FormatState.Cbor -> mDocGenerator.getOpenid4VpX509CertificateTrust() - FormatState.SdJwt -> sdJwtGenerator.getOpenid4VpX509CertificateTrust() + is FormatState.Cbor -> mDocGenerator.getOpenid4VpX509CertificateTrust() + is FormatState.SdJwt -> sdJwtGenerator.getOpenid4VpX509CertificateTrust() } - fun parseRequest(request: Request): RequestedDocumentData { return when (request) { is OpenId4VpRequest -> { - formatState = FormatState.Cbor + formatState = FormatState.Cbor(request.requestId != null) mDocGenerator.parseRequest(request) } is OpenId4VpSdJwtRequest -> { - formatState = FormatState.SdJwt + formatState = FormatState.SdJwt(request.requestId != null) sdJwtGenerator.parseRequest(request) } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt index c9b51577..2a0a152f 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt @@ -36,11 +36,17 @@ import eu.europa.ec.eudi.wallet.issue.openid4vci.DocumentManagerSdJwt import eu.europa.ec.eudi.wallet.keystore.KeyGenerator import eu.europa.ec.eudi.wallet.keystore.KeyGeneratorImpl import eu.europa.ec.eudi.wallet.transfer.openid4vp.OpenId4VpSdJwtRequest +import eu.europa.ec.eudi.wallet.util.ZKP_ISSUER_CERT +import eu.europa.ec.eudi.wallet.util.getECPublicKeyFromCert import eu.europa.ec.eudi.wallet.util.parseCertificateFromSdJwt +import eu.europa.ec.eudi.wallet.zkp.network.ZKPClient import kotlinx.coroutines.runBlocking +import software.tice.ZKPGenerator +import software.tice.ZKPProverSdJwt import java.io.ByteArrayInputStream import java.security.cert.CertificateFactory import java.security.cert.X509Certificate +import java.util.Base64 class OpenId4VpSdJwtResponseGeneratorImpl( private val documentsResolver: DocumentsResolver, @@ -50,6 +56,8 @@ class OpenId4VpSdJwtResponseGeneratorImpl( private val sdJwtNamespace = "eu.europa.ec.eudi.pid.1" + private var zkpRequestId: String? = null + override fun createResponse(disclosedDocuments: DisclosedDocuments) = runBlocking { val disclosedDocument = disclosedDocuments.documents.first() @@ -80,7 +88,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( certificateFactory.generateCertificate(ByteArrayInputStream(key.certificate.encoded)) as X509Certificate val ecKey = ECKey.parse(certificate) - val string = presentationSdJwt!!.serializeWithKeyBinding( + val jwt = presentationSdJwt!!.serializeWithKeyBinding( jwtSerializer = { it.first }, hashAlgorithm = HashAlgorithm.SHA_256, keyBindingSigner = object : KeyBindingSigner { @@ -95,7 +103,20 @@ class OpenId4VpSdJwtResponseGeneratorImpl( claimSetBuilderAction = { } ) - return@runBlocking ResponseResult.Success(DeviceResponse(string.toByteArray())) + val jwtByteArray = if (zkpRequestId == null) { + jwt.toByteArray() + } else { + val zkpKey = getECPublicKeyFromCert(ZKP_ISSUER_CERT) + val prover = ZKPProverSdJwt(ZKPGenerator(zkpKey)) + val challenge = ZKPClient().getChallenges( + zkpRequestId!!, + listOf(sdJwtNamespace to prover.createChallengeRequest(jwt)), + ).first().second + val zkpJwt = prover.answerChallenge(challenge, jwt) + zkpJwt.toByteArray() + } + + return@runBlocking ResponseResult.Success(DeviceResponse(jwtByteArray)) } override fun setReaderTrustStore(readerTrustStore: ReaderTrustStore) = apply { @@ -106,10 +127,11 @@ class OpenId4VpSdJwtResponseGeneratorImpl( internal fun getOpenid4VpX509CertificateTrust() = openid4VpX509CertificateTrust override fun parseRequest(request: OpenId4VpSdJwtRequest): RequestedDocumentData { + zkpRequestId = request.requestId val inputDescriptors = request.openId4VPAuthorization.presentationDefinition.inputDescriptors .filter { inputDescriptor -> - inputDescriptor.format?.json?.contains("vc_sd_jwt") == true + inputDescriptor.format?.json?.contains("vc+sd-jwt") == true } if (inputDescriptors.isEmpty()) { diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt new file mode 100644 index 00000000..5da060f0 --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt @@ -0,0 +1,40 @@ +package eu.europa.ec.eudi.wallet.util + +import com.nimbusds.jose.jwk.ECKey +import org.json.JSONObject +import java.io.ByteArrayInputStream +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.security.interfaces.ECPublicKey +import java.util.Base64 + +const val ZKP_ISSUER_CERT = "-----BEGIN CERTIFICATE-----\n" + + "MIICdDCCAhugAwIBAgIBAjAKBggqhkjOPQQDAjCBiDELMAkGA1UEBhMCREUxDzANBgNVBAcMBkJlcmxpbjEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxETAPBgNVBAsMCFQgQ1MgSURFMTYwNAYDVQQDDC1TUFJJTkQgRnVua2UgRVVESSBXYWxsZXQgUHJvdG90eXBlIElzc3VpbmcgQ0EwHhcNMjQwNTMxMDgxMzE3WhcNMjUwNzA1MDgxMzE3WjBsMQswCQYDVQQGEwJERTEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxCjAIBgNVBAsMAUkxMjAwBgNVBAMMKVNQUklORCBGdW5rZSBFVURJIFdhbGxldCBQcm90b3R5cGUgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOFBq4YMKg4w5fTifsytwBuJf/7E7VhRPXiNm52S3q1ETIgBdXyDK3kVxGxgeHPivLP3uuMvS6iDEc7qMxmvduKOBkDCBjTAdBgNVHQ4EFgQUiPhCkLErDXPLW2/J0WVeghyw+mIwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwLQYDVR0RBCYwJIIiZGVtby5waWQtaXNzdWVyLmJ1bmRlc2RydWNrZXJlaS5kZTAfBgNVHSMEGDAWgBTUVhjAiTjoDliEGMl2Yr+ru8WQvjAKBggqhkjOPQQDAgNHADBEAiAbf5TzkcQzhfWoIoyi1VN7d8I9BsFKm1MWluRph2byGQIgKYkdrNf2xXPjVSbjW/U/5S5vAEC5XxcOanusOBroBbU=\n" + + "-----END CERTIFICATE-----" + +fun parseCertificateFromSdJwt(credential: String): X509Certificate { + val headerString = credential.split(".").first() + val headerJson = + JSONObject(String(Base64.getUrlDecoder().decode(headerString))) + val keyString = + headerJson.getJSONArray("x5c").getString(0).replace("\n", "") + + val pemKey = "-----BEGIN CERTIFICATE-----\n" + + "${keyString}\n" + + "-----END CERTIFICATE-----" + + return x509Certificate(pemKey) +} + + +@Throws(com.nimbusds.jose.JOSEException::class) +fun getECPublicKeyFromCert(certString: String): ECPublicKey { + val certificate = x509Certificate(certString) + val ecKey = ECKey.parse(certificate) + return ecKey.toECPublicKey() +} + +fun x509Certificate(pemKey: String): X509Certificate { + val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + return certificateFactory.generateCertificate(ByteArrayInputStream(pemKey.toByteArray())) as X509Certificate +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt deleted file mode 100644 index 894b20f4..00000000 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/SdJwtUtil.kt +++ /dev/null @@ -1,23 +0,0 @@ -package eu.europa.ec.eudi.wallet.util - -import org.json.JSONObject -import java.io.ByteArrayInputStream -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.Base64 - -public fun parseCertificateFromSdJwt(credential: String): X509Certificate { - val headerString = credential.split(".").first() - val headerJson = - JSONObject(String(Base64.getUrlDecoder().decode(headerString))) - val keyString = - headerJson.getJSONArray("x5c").getString(0).replace("\n", "") - - val pemKey = "-----BEGIN CERTIFICATE-----\n" + - "${keyString}\n" + - "-----END CERTIFICATE-----" - - val certificateFactory: CertificateFactory = - CertificateFactory.getInstance("X.509") - return certificateFactory.generateCertificate(ByteArrayInputStream(pemKey.toByteArray())) as X509Certificate -} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt new file mode 100644 index 00000000..6a401696 --- /dev/null +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt @@ -0,0 +1,102 @@ +package eu.europa.ec.eudi.wallet.zkp.network + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.util.Base64URL +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.http.Body +import retrofit2.http.Field +import retrofit2.http.POST +import retrofit2.http.Path +import software.tice.ChallengeRequestData +import java.security.interfaces.ECPublicKey + +class ZKPClient( + baseUrl: String = "https://staging.verifier.wallet.tice.software/wallet/zkp/", +) { + private val retrofit: Retrofit = Retrofit + .Builder() + .baseUrl(baseUrl) + .addConverterFactory(Json.asConverterFactory("application/json; charset=UTF8".toMediaType())) + .build() + private val service: ZkpAPIService = retrofit.create(ZkpAPIService::class.java) + + suspend fun getChallenges( + zkpRequestId: String, + requestData: List> + ): List> { + val response = service.requestZkp( + zkpRequestId, + requestData.map { + ZkpRequest( + it.first, + it.second.digest, + it.second.r, + "secp256r1-sha256", + ) + }, + ) + + return response.body()?.map { + it.id to ECKey.Builder( + Curve(it.crv), + Base64URL.from(it.x), + Base64URL.from(it.y), + ) + .keyID(it.kid) + .keyUse(KeyUse.SIGNATURE) + .build() + .toECPublicKey() + } ?: throw IllegalArgumentException("Missing response body") + } +} + +interface ZkpAPIService { + @POST("{zkpRequestId}/jwks.json") + suspend fun requestZkp( + @Path("zkpRequestId") zkpRequestId: String, + @Body request: List, + ): Response> +} + +@Serializable +data class ZkpRequest( + val id: String, + val digest: String, + val r: String, + @Field("proof_type") val proofType: String, +) +// [ +// { +// "id": "eu.europa.ec.eudiw.pid.1", +// "digest": "_4KGX6aVS8B0T6Hewpr1H9h9-gjkjOyu8A6fb85GE2w=", +// "r": "C7U9q9o7dkDEhcOYXO9yfckQYoRxYs8z6POaac6EBjM=", +// "proof_type": "secp256r1-sha256" +// } +//] + +@Serializable +data class ZkpResponse( + val id: String, // WIP unused + val kid: String, + val kty: String, // WIP unused + val crv: String, + val x: String, + val y: String, +) +// [ +// { +// "id": "eu.europa.ec.eudiw.pid.1", +// "kid": "eu.europa.ec.eudiw.pid.1", +// "kty": "EC", +// "crv": "P-256", +// "x": "jjR25_Day4VGHGGSn0uW_dzS7dVO0at8xh8gZ8z992A", +// "y": "16LSQqNZzvs5BEIqsdnkWw127L0j3ThNbiMAN0BIQQY" +// } +//] \ No newline at end of file From 8676c49f6fffbc16cabe69bd3082e561553c8f1d Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Fri, 23 Aug 2024 16:53:55 +0200 Subject: [PATCH 6/8] Fix sdjwt zkp --- .../OpenId4VpSdJwtResponseGeneratorImpl.kt | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt index 2a0a152f..529bc88a 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpSdJwtResponseGeneratorImpl.kt @@ -46,7 +46,6 @@ import software.tice.ZKPProverSdJwt import java.io.ByteArrayInputStream import java.security.cert.CertificateFactory import java.security.cert.X509Certificate -import java.util.Base64 class OpenId4VpSdJwtResponseGeneratorImpl( private val documentsResolver: DocumentsResolver, @@ -65,6 +64,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( ?: throw IllegalArgumentException() val sdJwt = getSdJwtFromCredentials(credentials) + sdJwt.jwt val jsonPointer = disclosedDocument.docRequest.requestItems.mapNotNull { item -> JsonPointer.parse(item.elementIdentifier) @@ -88,8 +88,21 @@ class OpenId4VpSdJwtResponseGeneratorImpl( certificateFactory.generateCertificate(ByteArrayInputStream(key.certificate.encoded)) as X509Certificate val ecKey = ECKey.parse(certificate) - val jwt = presentationSdJwt!!.serializeWithKeyBinding( - jwtSerializer = { it.first }, + val presentationJwt = presentationSdJwt!!.serializeWithKeyBinding( + jwtSerializer = { + if (zkpRequestId == null) it.first else { + runBlocking { + val zkpKey = getECPublicKeyFromCert(ZKP_ISSUER_CERT) + val prover = ZKPProverSdJwt(ZKPGenerator(zkpKey)) + val challenge = ZKPClient().getChallenges( + zkpRequestId!!, + listOf(sdJwtNamespace to prover.createChallengeRequest(it.first)), + ).first().second + val zkpJwt = prover.answerChallenge(challenge, it.first) + zkpJwt + } + } + }, hashAlgorithm = HashAlgorithm.SHA_256, keyBindingSigner = object : KeyBindingSigner { override val signAlgorithm: JWSAlgorithm = JWSAlgorithm.ES256 @@ -103,20 +116,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( claimSetBuilderAction = { } ) - val jwtByteArray = if (zkpRequestId == null) { - jwt.toByteArray() - } else { - val zkpKey = getECPublicKeyFromCert(ZKP_ISSUER_CERT) - val prover = ZKPProverSdJwt(ZKPGenerator(zkpKey)) - val challenge = ZKPClient().getChallenges( - zkpRequestId!!, - listOf(sdJwtNamespace to prover.createChallengeRequest(jwt)), - ).first().second - val zkpJwt = prover.answerChallenge(challenge, jwt) - zkpJwt.toByteArray() - } - - return@runBlocking ResponseResult.Success(DeviceResponse(jwtByteArray)) + return@runBlocking ResponseResult.Success(DeviceResponse(presentationJwt.toByteArray())) } override fun setReaderTrustStore(readerTrustStore: ReaderTrustStore) = apply { From c9d2e6fd708b4bd4ddb6c789f1b757fc6776644b Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Mon, 26 Aug 2024 10:31:29 +0200 Subject: [PATCH 7/8] Cleanup --- .../wallet/issue/openid4vci/DefaultOffer.kt | 1 + .../wallet/issue/openid4vci/OfferCreator.kt | 1 + .../ec/eudi/wallet/keystore/KeyGenerator.kt | 4 ---- .../transfer/openid4vp/OpenId4VpRequest.kt | 13 ----------- .../OpenId4VpCBORResponseGeneratorImpl.kt | 6 ++--- .../ec/eudi/wallet/util/CertificateUtil.kt | 1 - .../ec/eudi/wallet/zkp/network/ZKPClient.kt | 22 ++----------------- 7 files changed, 7 insertions(+), 41 deletions(-) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt index 814f24e2..175d6f34 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultOffer.kt @@ -41,6 +41,7 @@ internal data class DefaultOffer( override val offeredDocuments: List get() = issuerMetadata.credentialConfigurationsSupported + // TODO temporarily removed to make it work //.filterKeys { it in credentialOffer.credentialConfigurationIdentifiers } //.filterValues { credentialConfigurationFilter(it) } .map { (id, conf) -> DefaultOfferedDocument(id, conf) } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt index 27d0eb4c..0d1fe1f6 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/OfferCreator.kt @@ -44,6 +44,7 @@ internal class OfferCreator( DocTypeFilter(docType), ProofTypeFilter(config.proofTypes) ) + // TODO temporarily removed to make it work // val credentialConfigurationId = // credentialIssuerMetadata.credentialConfigurationsSupported.filterValues { conf -> // credentialConfigurationFilter(conf) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt index 71da144d..f4b1a5f6 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt @@ -62,12 +62,8 @@ internal object KeyGeneratorImpl : KeyGenerator { }.toBase64String() } catch (exception: NoSuchAlgorithmException) { throw SignatureException(exception) -// throw SigningException("Signing failed.", exception) } catch (exception: InvalidKeyException) { throw SignatureException(exception) -// throw SigningException("Signing failed.", exception) -// } catch (exception: SignatureException) { -// throw SigningException("Signing failed.", exception) } private fun ByteArray.toBase64String() = String(Base64.encode(this, Base64.DEFAULT)) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt index 3d5d5db3..d9d277f7 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/OpenId4VpRequest.kt @@ -25,20 +25,7 @@ class OpenId4VpRequest( val requestId: String? = null, ) : Request -//class OpenId4VpZkpRequest( -// val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, -// val sessionTranscript: SessionTranscriptBytes, -// val requestId: String, -//) : Request - - class OpenId4VpSdJwtRequest( val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, val requestId: String? = null, ) : Request - -//class OpenId4VpSdJwtZkpRequest( -// val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, -// val requestId: String, -//) : Request - diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt index fcdec912..da2006b0 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/transfer/openid4vp/responseGenerator/OpenId4VpCBORResponseGeneratorImpl.kt @@ -112,7 +112,7 @@ class OpenId4VpCBORResponseGeneratorImpl( request.openId4VPAuthorization.presentationDefinition.inputDescriptors .mapNotNull { inputDescriptor -> inputDescriptor.format?.jsonObject() - ?.takeIf { it.containsKey("mso_mdoc") || it.containsKey("mso_mdoc+zkp")} // ignore formats other than "mso_mdoc" + ?.takeIf { it.containsKey("mso_mdoc") || it.containsKey("mso_mdoc+zkp") } // ignore formats other than "mso_mdoc" ?.run { inputDescriptor.id.value.trim() to inputDescriptor.constraints.fields() .mapNotNull { fieldConstraint -> @@ -204,9 +204,9 @@ class OpenId4VpCBORResponseGeneratorImpl( zkpRequestId!!, requestData, ) - challenges?.forEach { + challenges.forEach { deviceResponse.addDocument( - Base64.getDecoder().decode(prover.answerChallenge(it.second, it.first)) + prover.answerChallenge(it.second, it.first).toByteArray() ) } } diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt index 5da060f0..3160fc05 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/util/CertificateUtil.kt @@ -26,7 +26,6 @@ fun parseCertificateFromSdJwt(credential: String): X509Certificate { return x509Certificate(pemKey) } - @Throws(com.nimbusds.jose.JOSEException::class) fun getECPublicKeyFromCert(certString: String): ECPublicKey { val certificate = x509Certificate(certString) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt index 6a401696..4b0b9fd7 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt @@ -72,31 +72,13 @@ data class ZkpRequest( val r: String, @Field("proof_type") val proofType: String, ) -// [ -// { -// "id": "eu.europa.ec.eudiw.pid.1", -// "digest": "_4KGX6aVS8B0T6Hewpr1H9h9-gjkjOyu8A6fb85GE2w=", -// "r": "C7U9q9o7dkDEhcOYXO9yfckQYoRxYs8z6POaac6EBjM=", -// "proof_type": "secp256r1-sha256" -// } -//] @Serializable data class ZkpResponse( - val id: String, // WIP unused + val id: String, val kid: String, - val kty: String, // WIP unused + val kty: String, val crv: String, val x: String, val y: String, ) -// [ -// { -// "id": "eu.europa.ec.eudiw.pid.1", -// "kid": "eu.europa.ec.eudiw.pid.1", -// "kty": "EC", -// "crv": "P-256", -// "x": "jjR25_Day4VGHGGSn0uW_dzS7dVO0at8xh8gZ8z992A", -// "y": "16LSQqNZzvs5BEIqsdnkWw127L0j3ThNbiMAN0BIQQY" -// } -//] \ No newline at end of file From 143dea7433c1dfd60cc03d635a0c4f4856a85ea6 Mon Sep 17 00:00:00 2001 From: Ronny Brzeski Date: Mon, 26 Aug 2024 11:32:46 +0200 Subject: [PATCH 8/8] Change reviewer remarks --- gradle/libs.versions.toml | 2 -- wallet-core/build.gradle | 1 - .../openid4vci/CredentialConfigurationFilter.kt | 12 +++--------- .../eudi/wallet/issue/openid4vci/ProcessResponse.kt | 1 - .../europa/ec/eudi/wallet/keystore/KeyGenerator.kt | 3 +-- .../europa/ec/eudi/wallet/zkp/network/ZKPClient.kt | 2 +- 6 files changed, 5 insertions(+), 16 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3f2668dd..c7276fb5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,6 @@ junit = "4.13.2" junit-android = "1.1.5" kotlin = "1.8.10" ktor = "2.3.5" -#ktorClientSerialization = "2.3.6" mavenPublish = "0.25.3" mockito-android = "4.0.0" mockito-inline = "4.0.0" @@ -63,7 +62,6 @@ junit = { module = "junit:junit", version.ref = "junit" } junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } -#ktor-client-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktorClientSerialization" } mockito-android = { module = "org.mockito:mockito-android", version.ref = "mockito-android" } mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-inline" } diff --git a/wallet-core/build.gradle b/wallet-core/build.gradle index 0757ee6a..45e8b3e9 100644 --- a/wallet-core/build.gradle +++ b/wallet-core/build.gradle @@ -149,7 +149,6 @@ dependencies { // Ktor Android Engine runtimeOnly libs.ktor.client.android -// implementation libs.ktor.client.serialization implementation libs.ktor.client.logging // Bouncy Castle diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt index 5fb660ab..e8e4471b 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilter.kt @@ -53,14 +53,6 @@ internal fun interface CredentialConfigurationFilter { internal val MsoMdocFormatFilter: CredentialConfigurationFilter = FormatFilter(MsoMdocCredential::class) - /** - * Filter for [CredentialConfiguration] instances for sd-jwt format - */ - @JvmSynthetic - internal val SdJwtFormatFilter: CredentialConfigurationFilter = - FormatFilter(SdJwtVcCredential::class) - - internal val SdJwtOrMsoMdocFormatFilter: CredentialConfigurationFilter = CredentialConfigurationFilter { it.instanceOf(SdJwtVcCredential::class) || it.instanceOf(MsoMdocCredential::class) @@ -73,7 +65,9 @@ internal fun interface CredentialConfigurationFilter { */ @JvmSynthetic internal fun DocTypeFilter(docType: String): CredentialConfigurationFilter = - Compose(SdJwtOrMsoMdocFormatFilter, CredentialConfigurationFilter { conf -> conf.docType == docType }) + Compose( + SdJwtOrMsoMdocFormatFilter, + CredentialConfigurationFilter { conf -> conf.docType == docType }) /** * Filter for [CredentialConfiguration] instances based on the proof type. diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt index c5faea07..0c736df8 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/ProcessResponse.kt @@ -122,7 +122,6 @@ internal class ProcessResponse( } else { val cborBytes = Base64.getUrlDecoder().decode(credential.credential) - logger?.d(TAG, "CBOR bytes: ${Hex.toHexString(cborBytes)}") documentManager.storeIssuedDocument( unsignedDocument, cborBytes diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt index f4b1a5f6..8dd9d10b 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/keystore/KeyGenerator.kt @@ -22,6 +22,7 @@ import java.security.cert.CertificateException private const val ANDROID_KEY_STORE = "AndroidKeyStore" public const val DEV_KEY_ALIAS = "eudi_wallet_dev_key" +private const val SIGNATURE_ALGORITHM = "SHA256withECDSA" interface KeyGenerator { @RequiresApi(Build.VERSION_CODES.R) @@ -46,8 +47,6 @@ internal object KeyGeneratorImpl : KeyGenerator { return entry } - private const val SIGNATURE_ALGORITHM = "SHA256withECDSA" - @Throws(SignatureException::class) override fun sign( key: PrivateKey, diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt index 4b0b9fd7..e748f67a 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/zkp/network/ZKPClient.kt @@ -77,7 +77,7 @@ data class ZkpRequest( data class ZkpResponse( val id: String, val kid: String, - val kty: String, + val kty: String, val crv: String, val x: String, val y: String,