diff --git a/build.gradle b/build.gradle index 0e57aa3e..8c3c1a20 100644 --- a/build.gradle +++ b/build.gradle @@ -5,4 +5,4 @@ plugins { alias libs.plugins.dokka apply false alias libs.plugins.dependency.license.report apply false alias libs.plugins.dependencycheck apply false -} +} \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt index ffd87180..54ae1e02 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/document/Constants.kt @@ -31,4 +31,12 @@ object Constants { // mDL const val MDL_DOCTYPE = "org.iso.18013.5.1.mDL" const val MDL_NAMESPACE = "org.iso.18013.5.1" + + // mdoc + const val MDOC_FORMAT = "mso_mdoc" + const val MDOC_FORMAT_ZKP = "mso_mdoc+zkp" + + // sdjwt + const val SDJWT_FORMAT = "vc+sd-jwt" + const val SDJWT_FORMAT_ZKP = "vc+sd-jwt+zkp" } \ No newline at end of file diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/AuthorizationResponse.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/AuthorizationResponse.kt index fca39a0b..c35ade5b 100644 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/AuthorizationResponse.kt +++ b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/AuthorizationResponse.kt @@ -7,8 +7,9 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci * @constructor Creates a new [AuthorizationResponse] instance. * @param authorizationCode the authorization code * @param serverState the server state + * @param dpopNonce the dpopNonce */ -public data class AuthorizationResponse( +data class AuthorizationResponse( val authorizationCode: String, val serverState: String, val dpopNonce: String 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 e8e4471b..5eed215a 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 @@ -31,7 +31,7 @@ internal fun interface CredentialConfigurationFilter { /** * Companion object for [CredentialConfigurationFilter] instances. - * @property MsoMdocFormatFilter filter for [CredentialConfiguration] instances for mso_mdoc format + * @property SdJwtOrMsoMdocFormatFilter filter for [CredentialConfiguration] instances for mso_mdoc or sdjwt format */ companion object { @@ -47,12 +47,8 @@ internal fun interface CredentialConfigurationFilter { } /** - * Filter for [CredentialConfiguration] instances for mso_mdoc format + * Filter for [CredentialConfiguration] instances for mso_mdoc format and sdjwt */ - @JvmSynthetic - internal val MsoMdocFormatFilter: CredentialConfigurationFilter = - FormatFilter(MsoMdocCredential::class) - internal val SdJwtOrMsoMdocFormatFilter: CredentialConfigurationFilter = CredentialConfigurationFilter { it.instanceOf(SdJwtVcCredential::class) || it.instanceOf(MsoMdocCredential::class) diff --git a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultBrowserAuthorizationHandler.kt b/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultBrowserAuthorizationHandler.kt deleted file mode 100644 index 5aef70b1..00000000 --- a/wallet-core/src/main/java/eu/europa/ec/eudi/wallet/issue/openid4vci/DefaultBrowserAuthorizationHandler.kt +++ /dev/null @@ -1,84 +0,0 @@ -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.AuthorizationRequestPrepared -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume - -class DefaultBrowserAuthorizationHandler( - private val context: Context -) : AuthorizationHandler { - private var cancellableContinuation: CancellableContinuation>? = - null - - /** - * Resume the authorization flow after the user has been redirected back to the app - * @param intent the intent that contains the authorization code - * @throws [IllegalStateException] if no authorization request to resume - * - */ - fun resumeWithAuthorization(intent: Intent) { - intent.data?.let { handleUri(it) } - ?: throw IllegalStateException("No authorization uri found") - } - - /** - * Resume the authorization flow after the user has been redirected back to the app - * @param uri the uri that contains the authorization code - * @throws [IllegalStateException] if no authorization request to resume - * - */ - fun resumeWithAuthorization(uri: String) { - handleUri(Uri.parse(uri)) - } - - /** - * Resume the authorization flow after the user has been redirected back to the app - * @param uri the uri that contains the authorization code - * @throws [IllegalStateException] if no authorization request to resume - * - */ - fun resumeWithAuthorization(uri: Uri) { - handleUri(uri) - } - - private fun handleUri(uri: Uri) { - val code = uri.getQueryParameter("code") - ?: throw IllegalStateException("No authorization code found") - val state = - uri.getQueryParameter("state") ?: throw IllegalStateException("No server state found") - - cancellableContinuation?.resume( - Result.success( - AuthorizationResponse( - code, - state, - "dpopNonce" - ) - ) - ) - } - - override suspend fun doAuthorization( - authorizationCodeRequest: AuthorizationRequestPrepared - ): Result { - cancellableContinuation?.cancel() - - return suspendCancellableCoroutine { continuation -> - cancellableContinuation = continuation - val authorizationCodeUri = - Uri.parse(authorizationCodeRequest.authorizationCodeURL.value.toString()) - - context.startActivity(Intent(Intent.ACTION_VIEW, authorizationCodeUri).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - }) - } - } - - override fun cancelAuthorization() { - cancellableContinuation?.cancel() - } -} \ No newline at end of file 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 175d6f34..e597484e 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 @@ -17,6 +17,7 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci import eu.europa.ec.eudi.openid4vci.* +import eu.europa.ec.eudi.wallet.document.Constants.EU_PID_DOCTYPE /** * Default implementation of [Offer]. @@ -41,9 +42,6 @@ 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) } override val txCodeSpec: Offer.TxCodeSpec? @@ -93,7 +91,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.1" + is SdJwtVcCredential -> EU_PID_DOCTYPE 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 index 39ed3119..c8134e30 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 @@ -78,7 +78,6 @@ private fun String.toDocument( val vct = payloadJson.getString("vct") val docName = "Personalausweis" - val data = payloadJson.toString() SdJwtDocument( id = id, 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 0d1fe1f6..2b09d326 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,12 +44,6 @@ internal class OfferCreator( DocTypeFilter(docType), ProofTypeFilter(config.proofTypes) ) - // TODO temporarily removed to make it work -// val credentialConfigurationId = -// credentialIssuerMetadata.credentialConfigurationsSupported.filterValues { conf -> -// credentialConfigurationFilter(conf) -// }.keys.firstOrNull() ?: throw IllegalStateException("No suitable configuration found") - val credentialOffer = CredentialOffer( credentialIssuerIdentifier = credentialIssuerId, credentialIssuerMetadata = credentialIssuerMetadata, 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 c54bacbb..73705485 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 @@ -18,10 +18,9 @@ package eu.europa.ec.eudi.wallet.issue.openid4vci 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 io.ktor.client.HttpClient import org.jetbrains.annotations.VisibleForTesting internal class OfferResolver( 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 0c736df8..515038f9 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 @@ -28,8 +28,6 @@ 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.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 @@ -38,7 +36,6 @@ 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.Closeable import java.util.Base64 @@ -64,7 +61,7 @@ internal class ProcessResponse( } } - suspend fun process( + private suspend fun process( unsignedDocument: UnsignedDocument, outcomeResult: Result ) { @@ -90,61 +87,15 @@ internal class ProcessResponse( continuations.values.forEach { it.cancel() } } - fun processSubmittedRequest(unsignedDocument: UnsignedDocument, outcome: SubmissionOutcome) { + private fun processSubmittedRequest( + unsignedDocument: UnsignedDocument, + outcome: SubmissionOutcome + ) { when (outcome) { - is SubmissionOutcome.Success -> when (val credential = outcome.credentials[0]) { - is IssuedCredential.Issued -> try { - if (isSdJwt(credential.credential)) { - val certificate = parseCertificateFromSdJwt(credential.credential) - - 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 - ) - documentManager.deleteDocumentById(unsignedDocument.id) - listener.invoke( - IssueEvent.DocumentIssued( - unsignedDocument.id, - unsignedDocument.name, - unsignedDocument.docType - ) - ) - } - } else { - val cborBytes = Base64.getUrlDecoder().decode(credential.credential) - - documentManager.storeIssuedDocument( - unsignedDocument, - cborBytes - ).notifyListener(unsignedDocument) - } - } catch (e: Throwable) { - 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() - ) - .notifyListener(unsignedDocument, isDeferred = true) - } - } + is SubmissionOutcome.Success -> processSubmittedRequestSuccess( + outcome, + unsignedDocument + ) is SubmissionOutcome.InvalidProof -> { documentManager.deleteDocumentById(unsignedDocument.id) @@ -163,16 +114,89 @@ internal class ProcessResponse( } } + private fun processSubmittedRequestSuccess( + outcome: SubmissionOutcome.Success, + unsignedDocument: UnsignedDocument + ) { + when (val credential = outcome.credentials[0]) { + is IssuedCredential.Issued -> try { + if (isSdJwt(credential.credential)) { + processIssuedSdjwt(credential, unsignedDocument) + } else { + processIssuedMdoc(credential, unsignedDocument) + } + } catch (e: Throwable) { + 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() + ).notifyListener(unsignedDocument, isDeferred = true) + } + } + } + private fun isSdJwt(credential: String): Boolean { return try { val headerString = credential.split(".").first() - val headerJson = JSONObject(String(Base64.getUrlDecoder().decode(headerString))) + // try to parse to header json + JSONObject(String(Base64.getUrlDecoder().decode(headerString))) true } catch (e: Exception) { false } } + private fun processIssuedSdjwt( + credential: IssuedCredential.Issued, + unsignedDocument: UnsignedDocument + ) { + val certificate = parseCertificateFromSdJwt(credential.credential) + + 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 + ) + documentManager.deleteDocumentById(unsignedDocument.id) + listener.invoke( + IssueEvent.DocumentIssued( + unsignedDocument.id, + unsignedDocument.name, + unsignedDocument.docType + ) + ) + } + } + + private fun processIssuedMdoc( + credential: IssuedCredential.Issued, + unsignedDocument: UnsignedDocument + ) { + val cborBytes = Base64.getUrlDecoder().decode(credential.credential) + + documentManager.storeIssuedDocument( + unsignedDocument, + cborBytes + ).notifyListener(unsignedDocument) + } + private fun UserAuthRequiredException.toIssueEvent( unsignedDocument: UnsignedDocument, ): IssueEvent.DocumentRequiresUserAuth { 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 8dd9d10b..1524ebc4 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 @@ -21,7 +21,7 @@ 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" +private const val DEV_KEY_ALIAS = "eudi_wallet_dev_key" private const val SIGNATURE_ALGORITHM = "SHA256withECDSA" interface KeyGenerator { @@ -92,7 +92,7 @@ internal object KeyGeneratorImpl : KeyGenerator { } @Throws(KeyStoreException::class) - public fun getKeyStore(): KeyStore = try { + fun getKeyStore(): KeyStore = try { KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } } catch (exception: KeyStoreException) { throw KeyStoreException("Get KeyStore instance failed.", exception) 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 d9d277f7..203c7fb0 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 @@ -28,4 +28,4 @@ class OpenId4VpRequest( class OpenId4VpSdJwtRequest( val openId4VPAuthorization: ResolvedRequestObject.OpenId4VPAuthorization, val requestId: String? = null, -) : Request +) : Request \ No newline at end of file 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 d4a12518..f36857f8 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 @@ -43,6 +43,10 @@ import eu.europa.ec.eudi.prex.DescriptorMap import eu.europa.ec.eudi.prex.Id import eu.europa.ec.eudi.prex.JsonPath import eu.europa.ec.eudi.prex.PresentationSubmission +import eu.europa.ec.eudi.wallet.document.Constants.MDOC_FORMAT +import eu.europa.ec.eudi.wallet.document.Constants.MDOC_FORMAT_ZKP +import eu.europa.ec.eudi.wallet.document.Constants.SDJWT_FORMAT +import eu.europa.ec.eudi.wallet.document.Constants.SDJWT_FORMAT_ZKP import eu.europa.ec.eudi.wallet.internal.Openid4VpUtils import eu.europa.ec.eudi.wallet.internal.mainExecutor import eu.europa.ec.eudi.wallet.logging.Logger @@ -234,14 +238,14 @@ class OpenId4vpManager( val format = requestObject.presentationDefinition.inputDescriptors.first().format?.jsonObject()?.keys?.first() val request = when (format) { - "mso_mdoc" -> { + MDOC_FORMAT -> { OpenId4VpRequest( requestObject, requestObject.toSessionTranscript() ) } - "mso_mdoc+zkp" -> { + MDOC_FORMAT_ZKP -> { OpenId4VpRequest( requestObject, requestObject.toSessionTranscript(), @@ -249,11 +253,11 @@ class OpenId4vpManager( ) } - "vc+sd-jwt" -> { + SDJWT_FORMAT -> { OpenId4VpSdJwtRequest(requestObject) } - "vc+sd-jwt+zkp" -> { + SDJWT_FORMAT_ZKP -> { OpenId4VpSdJwtRequest(requestObject, requestId) } @@ -378,7 +382,7 @@ class OpenId4vpManager( presentationDefinition.inputDescriptors.map { inputDescriptor -> DescriptorMap( inputDescriptor.id, - if (isZkp) "mso_mdoc+zkp" else "mso_mdoc", + if (isZkp) MDOC_FORMAT_ZKP else MDOC_FORMAT, path = JsonPath.jsonPath("$")!! ) } @@ -405,7 +409,7 @@ class OpenId4vpManager( presentationDefinition.inputDescriptors.map { inputDescriptor -> DescriptorMap( inputDescriptor.id, - if (isZkp) "vc+sd-jwt+zkp" else "vc+sd-jwt", + if (isZkp) SDJWT_FORMAT_ZKP else SDJWT_FORMAT, path = JsonPath.jsonPath("$")!! ) } 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 35a7e56f..d2830510 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 @@ -45,6 +45,8 @@ 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 import eu.europa.ec.eudi.openid4vp.legalName +import eu.europa.ec.eudi.wallet.document.Constants.MDOC_FORMAT +import eu.europa.ec.eudi.wallet.document.Constants.MDOC_FORMAT_ZKP import eu.europa.ec.eudi.wallet.internal.Openid4VpX509CertificateTrust import eu.europa.ec.eudi.wallet.logging.Logger import eu.europa.ec.eudi.wallet.logging.e @@ -62,7 +64,7 @@ import java.util.Base64 private const val TAG = "OpenId4VpCBORResponseGe" /** - * OpenId4VpCBORResponseGeneratorImpl class is used for parsing a request (Presentation Definition) and generating the DeviceResponse + * OpenId4VpCBORResponseGeneratorImpl class is used for parsing an mdoc request (Presentation Definition) and generating the DeviceResponse * * @param documentsResolver document manager instance * @param storageEngine storage engine used to store documents @@ -108,12 +110,11 @@ class OpenId4VpCBORResponseGeneratorImpl( 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") || it.containsKey("mso_mdoc+zkp") } // ignore formats other than "mso_mdoc" + ?.takeIf { it.containsKey(MDOC_FORMAT) || it.containsKey(MDOC_FORMAT_ZKP) } // ignore formats other than "mso_mdoc" ?.run { inputDescriptor.id.value.trim() to inputDescriptor.constraints.fields() .mapNotNull { fieldConstraint -> 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 9d904a65..ff39432b 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,7 +14,9 @@ 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 - +/** + * OpenId4VpResponseGeneratorDelegator class is used for delegating an mdoc or sdjwt request + */ class OpenId4VpResponseGeneratorDelegator( private val mDocGenerator: OpenId4VpCBORResponseGeneratorImpl, private val sdJwtGenerator: OpenId4VpSdJwtResponseGeneratorImpl @@ -25,7 +27,7 @@ class OpenId4VpResponseGeneratorDelegator( } companion object { - public var formatState: FormatState = FormatState.Cbor(false) + var formatState: FormatState = FormatState.Cbor(false) private set } 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 47232c66..1db00ad1 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 @@ -21,19 +21,18 @@ 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.openid4vci.SdJwtVcCredential 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.SdJwtDigest -import eu.europa.ec.eudi.sdjwt.SdJwtFactory 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.document.Constants.EU_PID_NAMESPACE +import eu.europa.ec.eudi.wallet.document.Constants.SDJWT_FORMAT import eu.europa.ec.eudi.wallet.internal.Openid4VpX509CertificateTrust import eu.europa.ec.eudi.wallet.issue.openid4vci.DocumentManagerSdJwt import eu.europa.ec.eudi.wallet.keystore.KeyGenerator @@ -47,24 +46,23 @@ import kotlinx.coroutines.runBlocking import software.tice.ZKPGenerator import software.tice.ZKPProverSdJwt import java.io.ByteArrayInputStream +import java.security.cert.CertificateException import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import java.util.Date +/** + * OpenId4VpSdJwtResponseGeneratorImpl class is used for parsing a sdjwt request (Presentation Definition) and generating the DeviceResponse + * + * @param documentsResolver document manager instance + */ class OpenId4VpSdJwtResponseGeneratorImpl( private val documentsResolver: DocumentsResolver, ) : ResponseGenerator() { private var readerTrustStore: ReaderTrustStore? = null private val openid4VpX509CertificateTrust = Openid4VpX509CertificateTrust(readerTrustStore) - private val sdJwtNamespace = "eu.europa.ec.eudi.pid.1" - private var zkpRequestId: String? = null - - private val CLAIM_NONCE = "nonce" - private val CLAIM_IAT = "iat" - private val CLAIM_AUD = "aud" - private var requestNonce: String? = null private var audience: String? = null @@ -84,20 +82,36 @@ 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 + try { + KeyGeneratorImpl.getSigningKey( + KeyGenerator.SigningKeyConfig( + KeyProperties.AUTH_DEVICE_CREDENTIAL, + 60 + ) ) - ) + } catch (exception: java.security.KeyStoreException) { + return@runBlocking ResponseResult.Failure(exception) + } } else { return@runBlocking ResponseResult.Failure(AndroidException("Build version to low.")) } - val certificateFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + val certificateFactory: CertificateFactory = try { + CertificateFactory.getInstance("X.509") + } catch (exception: CertificateException) { + return@runBlocking ResponseResult.Failure(exception) + } val certificate = - certificateFactory.generateCertificate(ByteArrayInputStream(key.certificate.encoded)) as X509Certificate - val ecKey = ECKey.parse(certificate) + try { + certificateFactory.generateCertificate(ByteArrayInputStream(key.certificate.encoded)) as X509Certificate + } catch (exception: CertificateException) { + return@runBlocking ResponseResult.Failure(exception) + } + val ecKey = try { + ECKey.parse(certificate) + } catch (exception: com.nimbusds.jose.JOSEException) { + return@runBlocking ResponseResult.Failure(exception) + } val presentationJwt = presentationSdJwt!!.serializeWithKeyBinding( jwtSerializer = { @@ -107,7 +121,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( val prover = ZKPProverSdJwt(ZKPGenerator(zkpKey)) val challenge = ZKPClient().getChallenges( zkpRequestId!!, - listOf(sdJwtNamespace to prover.createChallengeRequest(it.first)), + listOf(EU_PID_NAMESPACE to prover.createChallengeRequest(it.first)), ).first().second val zkpJwt = prover.answerChallenge(challenge, it.first) zkpJwt @@ -149,7 +163,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( val inputDescriptors = request.openId4VPAuthorization.presentationDefinition.inputDescriptors .filter { inputDescriptor -> - inputDescriptor.format?.json?.contains("vc+sd-jwt") == true + inputDescriptor.format?.json?.contains(SDJWT_FORMAT) == true } if (inputDescriptors.isEmpty()) { @@ -163,7 +177,7 @@ class OpenId4VpSdJwtResponseGeneratorImpl( .replace(".", "/") .drop(1) - sdJwtNamespace to elementIdentifier + EU_PID_NAMESPACE to elementIdentifier }.groupBy({ it.first }, { it.second }) .mapValues { (_, values) -> values.toList() } .toMap() @@ -221,4 +235,10 @@ class OpenId4VpSdJwtResponseGeneratorImpl( credentials ).getOrThrow() } + + private companion object { + const val CLAIM_NONCE = "nonce" + const val CLAIM_IAT = "iat" + const val CLAIM_AUD = "aud" + } } \ No newline at end of file diff --git a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt index 4ee48a35..425657db 100644 --- a/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt +++ b/wallet-core/src/test/java/eu/europa/ec/eudi/wallet/issue/openid4vci/CredentialConfigurationFilterTest.kt @@ -24,6 +24,7 @@ import eu.europa.ec.eudi.openid4vci.SdJwtVcCredential 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 eu.europa.ec.eudi.wallet.issue.openid4vci.OpenId4VciManager.Config.ProofType import io.mockk.every import io.mockk.mockk @@ -36,13 +37,13 @@ class CredentialConfigurationFilterTest { @Test fun `FormatFilter returns true for MsoMdocCredential`() { val credentialConfiguration = mockk(relaxed = true) - assertTrue(MsoMdocFormatFilter(credentialConfiguration)) + assertTrue(SdJwtOrMsoMdocFormatFilter(credentialConfiguration)) } @Test - fun `FormatFilter returns false for non-MsoMdocCredential`() { + fun `FormatFilter returns true for SdJwtVcCredential`() { val credentialConfiguration = mockk(relaxed = true) - assertFalse(MsoMdocFormatFilter(credentialConfiguration)) + assertTrue(SdJwtOrMsoMdocFormatFilter(credentialConfiguration)) }