From faf6e1cd361cdbe714d29144a3647ff87f1b58e8 Mon Sep 17 00:00:00 2001 From: "Josep Milan K.A" Date: Wed, 20 Nov 2024 17:21:59 +0530 Subject: [PATCH] Fix: Support for client id scheme x509_san_dns --- app/build.gradle.kts | 4 +- eudi-wallet-oidc-android/build.gradle.kts | 2 +- .../models/PresentationRequest.kt | 7 +- .../services/utils/X509SanRequestVerifier.kt | 232 +++++++++++++++ .../verification/VerificationService.kt | 271 +++++++++++++----- .../VerificationServiceInterface.kt | 3 +- 6 files changed, 445 insertions(+), 74 deletions(-) create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/utils/X509SanRequestVerifier.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d495df9..3dd4b88 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,7 +9,7 @@ android { defaultConfig { applicationId = "com.ewc.eudiwalletoidcandroid" - minSdk = 24 + minSdk = 26 targetSdk = 34 versionCode = 1 versionName = "1.0" @@ -72,7 +72,7 @@ dependencies { implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.3.1") - implementation("com.github.decentralised-dataexchange:presentation-exchange-sdk-android:2024.3.1") + implementation("com.github.decentralised-dataexchange:presentation-exchange-sdk-android:2024.11.1") implementation("org.slf4j:slf4j-api") { version { strictly("2.0.9") diff --git a/eudi-wallet-oidc-android/build.gradle.kts b/eudi-wallet-oidc-android/build.gradle.kts index d71a180..14072f8 100644 --- a/eudi-wallet-oidc-android/build.gradle.kts +++ b/eudi-wallet-oidc-android/build.gradle.kts @@ -9,7 +9,7 @@ android { compileSdk = 34 defaultConfig { - minSdk = 24 + minSdk = 26 testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles("consumer-rules.pro") diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationRequest.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationRequest.kt index fabadc6..00e0f38 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationRequest.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationRequest.kt @@ -20,5 +20,10 @@ data class PresentationRequest( @SerializedName("presentation_definition") var presentationDefinition: Any? = null, @SerializedName("presentation_definition_uri") var presentationDefinitionUri: String? = null, @SerializedName("client_metadata") var clientMetaDetails: Any? = null, - @SerializedName("client_metadata_uri") var clientMetadataUri: String? = null + @SerializedName("client_metadata_uri") var clientMetadataUri: String? = null, + @SerializedName("client_id_scheme") var clientIdScheme: String? = null +) +data class WrappedPresentationRequest( + var presentationRequest: PresentationRequest?=null, + var errorResponse: ErrorResponse? = null ) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/utils/X509SanRequestVerifier.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/utils/X509SanRequestVerifier.kt new file mode 100644 index 0000000..26da27d --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/utils/X509SanRequestVerifier.kt @@ -0,0 +1,232 @@ +package com.ewc.eudi_wallet_oidc_android.services.utils + +import android.util.Base64 +import org.json.JSONObject +import java.io.ByteArrayInputStream +import java.security.KeyStore +import java.security.PublicKey +import java.security.Signature +import java.security.cert.CertPathValidator +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.PKIXParameters +import java.security.cert.TrustAnchor +import java.security.cert.X509Certificate +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager +import java.util.Base64 as base64 + +class X509SanRequestVerifier private constructor() { + + companion object { + val instance = X509SanRequestVerifier() + } + + fun extractX5cFromJWT(jwt: String): List? { + val segments = jwt.split(".") + if (segments.size != 3) { + println("Invalid JWT format") + return null + } + + val headerSegment = segments[0] + val headerJson = String(Base64.decode(headerSegment, Base64.URL_SAFE)) + val headerMap = JSONObject(headerJson) + return if (headerMap.has("x5c")) { + val x5cArray = headerMap.getJSONArray("x5c") + List(x5cArray.length()) { x5cArray.getString(it) } + } else { + println("x5c not found in JWT header") + null + } + } + + fun validateClientIDInCertificate(x5cChain: List?, clientID: String?): Boolean { + val leafCertData = Base64.decode(x5cChain?.firstOrNull() ?: "", Base64.DEFAULT) + val certificate = CertificateFactory.getInstance("X.509") + .generateCertificate(leafCertData.inputStream()) as X509Certificate + + val dnsNames = extractDNSNamesFromCertificate(certificate) + return dnsNames.contains(clientID) + } + + private fun extractDNSNamesFromCertificate(certificate: X509Certificate): List { + val dnsNames = mutableListOf() + val sanList = certificate.subjectAlternativeNames ?: return dnsNames + + for (san in sanList) { + if (san[0] == 2) { // DNS Name + dnsNames.add(san[1] as String) + } + } + return dnsNames + } + + fun validateSignatureWithCertificate(jwt: String, x5cChain: List): Boolean { + val leafCertData = Base64.decode(x5cChain.firstOrNull() ?: "", Base64.DEFAULT) + val certificate = CertificateFactory.getInstance("X.509") + .generateCertificate(leafCertData.inputStream()) as X509Certificate + val publicKey = certificate.publicKey + + val segments = jwt.split(".") + if (segments.size != 3) { + println("Invalid JWT format") + return false + } + + val signedData = "${segments[0]}.${segments[1]}" + val signature = Base64.decode(base64UrlToBase64(segments[2]), Base64.DEFAULT) + + return verifySignature(publicKey, signedData.toByteArray(), signature) + } + + private fun base64UrlToBase64(base64Url: String): String { + var base64 = base64Url.replace('-', '+').replace('_', '/') + val padding = 4 - base64.length % 4 + if (padding != 4) { + base64 += "=".repeat(padding) + } + return base64 + } + + private fun verifySignature(publicKey: PublicKey, data: ByteArray, signature: ByteArray): Boolean { + return try { + val signatureInstance = Signature.getInstance("SHA256withRSA") + signatureInstance.initVerify(publicKey) + signatureInstance.update(data) + signatureInstance.verify(signature) + } catch (e: Exception) { + println("Signature verification failed: ${e.message}") + false + } + } + +// fun validateTrustChain(certificates: List): Boolean { +// val certificateFactory = CertificateFactory.getInstance("X.509") +// val x509Certificates = certificates.map { +// certificateFactory.generateCertificate(it.inputStream()) as X509Certificate +// } +// +// val certPath = certificateFactory.generateCertPath(x509Certificates) +// val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) +// trustManagerFactory.init(null as KeyStore?) // Use the default trust store +// +// // Retrieve the X509TrustManager to get the trusted issuers +// val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager +// val trustAnchors = trustManager.acceptedIssuers.map { TrustAnchor(it, null) }.toSet() +// +// val pkixParams = PKIXParameters(trustAnchors).apply { +// isRevocationEnabled = false // Disable revocation; adjust based on security needs +// } +// +// return try { +// val certPathValidator = CertPathValidator.getInstance("PKIX") +// certPathValidator.validate(certPath, pkixParams) +// true +// } catch (e: Exception) { +// println("Certificate path validation failed: ${e.message}") +// false +// } +// } + + +// fun validateTrustChain(x5cChain: List): Boolean { +// try { +// // Convert the Base64 encoded certificates to X509Certificate objects +// val certificateFactory = CertificateFactory.getInstance("X.509") +// val certificates = x5cChain.mapNotNull { certBase64 -> +// val certData = base64.getDecoder().decode(certBase64) +// try { +// certificateFactory.generateCertificate(ByteArrayInputStream(certData)) as X509Certificate +// } catch (e: Exception) { +// println("Invalid certificate in chain: ${e.message}") +// null +// } +// } +// +// // If the list of certificates is empty or contains invalid certificates, return false +// if (certificates.isEmpty()) { +// println("No valid certificates found in the chain.") +// return false +// } +// +// // Initialize TrustManagerFactory to get the default trust managers +// val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) +// trustManagerFactory.init(null as KeyStore?) // Uses the default system trust store +// +// // Find the first X509TrustManager +// val x509TrustManager = trustManagerFactory.trustManagers +// .filterIsInstance() +// .firstOrNull() +// ?: throw Exception("No X509TrustManager found in the TrustManagerFactory") +// +// // Create the CertPath from the certificates +// val certPath = certificateFactory.generateCertPath(certificates) +// +// // Create PKIXParameters using the accepted issuers from the trust manager +// val acceptedIssuers = x509TrustManager.acceptedIssuers +// .map { TrustAnchor(it, null) } +// .toSet() +// val pkixParams = java.security.cert.PKIXParameters(acceptedIssuers).apply { +// isRevocationEnabled = false // Adjust this based on your requirements +// } +// +// // Validate the certification path using PKIX +// val certPathValidator = CertPathValidator.getInstance("PKIX") +// try { +// certPathValidator.validate(certPath, pkixParams) +// println("The certificate chain is trusted.") +// return true +// } catch (e: CertPathValidatorException) { +// println("Certificate path validation failed: ${e.message}") +// return false +// } +// } catch (e: Exception) { +// println("An error occurred during trust chain validation: ${e.message}") +// return false +// } +// } + + @Throws(Exception::class) + fun validateTrustChain(x5cCertificates: List): Boolean { + val cf = CertificateFactory.getInstance("X.509") + + // Convert Base64 strings to X509Certificates + val x509Certificates = x5cCertificates.map { x5c -> + val certBytes = base64.getDecoder().decode(x5c) + cf.generateCertificate(ByteArrayInputStream(certBytes)) as X509Certificate + } + + // Create a custom KeyStore with the provided certificates as trusted anchors + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { + load(null, null) // Initialize an empty KeyStore + x509Certificates.forEachIndexed { index, cert -> + setCertificateEntry("cert$index", cert) // Add each certificate as a trusted entry + } + } + + // Initialize TrustManager with the custom KeyStore + val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + tmf.init(keyStore) + + // Create CertPath from X509Certificates + val certPath = cf.generateCertPath(x509Certificates) + + // Configure PKIXParameters to use only the provided anchors + val params = PKIXParameters(keyStore) + params.isRevocationEnabled = false // Disable revocation for simplicity + + // Validate CertPath + val cpv = CertPathValidator.getInstance("PKIX") + try{ + cpv.validate(certPath, params) + return true + }catch (e:Exception){ + println("${e.message}") + return false + } + + } + +} diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt index d889e09..1266575 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationService.kt @@ -16,6 +16,7 @@ import com.ewc.eudi_wallet_oidc_android.models.PresentationSubmission import com.ewc.eudi_wallet_oidc_android.models.PresentationSubmissionMdoc import com.ewc.eudi_wallet_oidc_android.models.VPTokenResponse import com.ewc.eudi_wallet_oidc_android.models.VpToken +import com.ewc.eudi_wallet_oidc_android.models.WrappedPresentationRequest import com.ewc.eudi_wallet_oidc_android.models.WrappedVpTokenResponse import com.ewc.eudi_wallet_oidc_android.services.issue.IssueService import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager @@ -23,6 +24,7 @@ import com.ewc.eudi_wallet_oidc_android.services.sdjwt.SDJWTService import com.ewc.eudi_wallet_oidc_android.services.utils.CborUtils import com.ewc.eudi_wallet_oidc_android.services.utils.JwtUtils.isValidJWT import com.ewc.eudi_wallet_oidc_android.services.utils.JwtUtils.parseJWTForPayload +import com.ewc.eudi_wallet_oidc_android.services.utils.X509SanRequestVerifier import com.github.decentraliseddataexchange.presentationexchangesdk.PresentationExchange import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential import com.google.gson.Gson @@ -35,13 +37,18 @@ import com.nimbusds.jose.crypto.Ed25519Signer import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.OctetKeyPair -import com.nimbusds.jose.shaded.json.parser.ParseException import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.json.JSONObject import java.util.Date import java.util.UUID - +import java.io.BufferedReader +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.URL +import java.nio.charset.StandardCharsets class VerificationService : VerificationServiceInterface { @@ -55,7 +62,7 @@ class VerificationService : VerificationServiceInterface { * * @return PresentationRequest */ - override suspend fun processAuthorisationRequest(data: String?): PresentationRequest? { + override suspend fun processAuthorisationRequest(data: String?): WrappedPresentationRequest? { if (data.isNullOrBlank()) return null @@ -67,7 +74,8 @@ class VerificationService : VerificationServiceInterface { val nonce = Uri.parse(data).getQueryParameter("nonce") val presentationDefinition = Uri.parse(data).getQueryParameter("presentation_definition") - val presentationDefinitionUri = Uri.parse(data).getQueryParameter("presentation_definition_uri") + val presentationDefinitionUri = + Uri.parse(data).getQueryParameter("presentation_definition_uri") val responseType = Uri.parse(data).getQueryParameter("response_type") val scope = Uri.parse(data).getQueryParameter("scope") val requestUri = Uri.parse(data).getQueryParameter("request_uri") @@ -75,6 +83,7 @@ class VerificationService : VerificationServiceInterface { val responseMode = Uri.parse(data).getQueryParameter("response_mode") val clientMetadataUri = Uri.parse(data).getQueryParameter("client_metadata_uri") val clientMetadataJson = Uri.parse(data).getQueryParameter("client_metadata") + val clientIdScheme = Uri.parse(data).getQueryParameter("client_id_scheme") val clientMetadetails: ClientMetaDetails? = if (!clientMetadataJson.isNullOrBlank()) { gson.fromJson(clientMetadataJson, ClientMetaDetails::class.java) } else { @@ -93,18 +102,23 @@ class VerificationService : VerificationServiceInterface { scope = scope, requestUri = requestUri, responseUri = responseUri, - clientMetaDetails = clientMetadetails + clientMetaDetails = clientMetadetails, + clientIdScheme = clientIdScheme ) - if (presentationDefinition.isNullOrBlank() && !presentationDefinitionUri.isNullOrBlank()){ - val resolvedPresentationDefinition = getPresentationDefinitionFromDefinitionUri(presentationDefinitionUri) + if (presentationDefinition.isNullOrBlank() && !presentationDefinitionUri.isNullOrBlank()) { + val resolvedPresentationDefinition = + getPresentationDefinitionFromDefinitionUri(presentationDefinitionUri) presentationRequest.presentationDefinition = resolvedPresentationDefinition } - if (clientMetadataJson.isNullOrBlank() && !clientMetadataUri.isNullOrBlank()){ - val resolvedClientMetaData = getClientMetaDataFromClientMetaDataUri(clientMetadataUri) + if (clientMetadataJson.isNullOrBlank() && !clientMetadataUri.isNullOrBlank()) { + val resolvedClientMetaData = + getClientMetaDataFromClientMetaDataUri(clientMetadataUri) presentationRequest.clientMetaDetails = resolvedClientMetaData } - return presentationRequest + return WrappedPresentationRequest(presentationRequest = presentationRequest,errorResponse = null) + } else if (!requestUri.isNullOrBlank() || !responseUri.isNullOrBlank()) { + val response = ApiManager.api.getService() ?.getPresentationDefinitionFromRequestUri(requestUri ?: responseUri ?: "") @@ -117,44 +131,52 @@ class VerificationService : VerificationServiceInterface { responseString, PresentationRequest::class.java ) - if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()){ - val resolvedPresentationDefinition = getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) + if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()) { + val resolvedPresentationDefinition = + getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) json.presentationDefinition = resolvedPresentationDefinition } - if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()){ - val resolvedClientMetaDetails = getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) + if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()) { + val resolvedClientMetaDetails = + getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) json.clientMetaDetails = resolvedClientMetaDetails } - return json - }else{ - if (isValidJWT(responseString?:"")) { + + return validatePresentationRequest(WrappedPresentationRequest(presentationRequest = json) , responseString) + } else { + if (isValidJWT(responseString ?: "")) { val json = gson.fromJson( - parseJWTForPayload(responseString?:"{}"), + parseJWTForPayload(responseString ?: "{}"), PresentationRequest::class.java ) - if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()){ - val resolvedPresentationDefinition = getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) + if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()) { + val resolvedPresentationDefinition = + getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) json.presentationDefinition = resolvedPresentationDefinition } - if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()){ - val resolvedClientMetaDetails = getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) + if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()) { + val resolvedClientMetaDetails = + getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) json.clientMetaDetails = resolvedClientMetaDetails } - return json - }else{ + return validatePresentationRequest(WrappedPresentationRequest(presentationRequest = json) , responseString) + } else { + val json = gson.fromJson( - responseString?:"{}", + responseString ?: "{}", PresentationRequest::class.java ) - if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()){ - val resolvedPresentationDefinition = getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) + if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()) { + val resolvedPresentationDefinition = + getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) json.presentationDefinition = resolvedPresentationDefinition } - if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()){ - val resolvedClientMetaDetails = getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) + if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()) { + val resolvedClientMetaDetails = + getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) json.clientMetaDetails = resolvedClientMetaDetails } - return json + return validatePresentationRequest(WrappedPresentationRequest(presentationRequest = json) , responseString) } } } else { @@ -162,24 +184,120 @@ class VerificationService : VerificationServiceInterface { } } else if (isValidJWT(data)) { val json = gson.fromJson( - parseJWTForPayload(data?:"{}"), + parseJWTForPayload(data ?: "{}"), PresentationRequest::class.java ) - if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()){ - val resolvedPresentationDefinition = getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) + if (json.presentationDefinition == null && !json.presentationDefinitionUri.isNullOrBlank()) { + val resolvedPresentationDefinition = + getPresentationDefinitionFromDefinitionUri(json.presentationDefinitionUri) json.presentationDefinition = resolvedPresentationDefinition } - if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()){ - val resolvedClientMetaDetails = getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) + if (json.clientMetaDetails == null && !json.clientMetadataUri.isNullOrBlank()) { + val resolvedClientMetaDetails = + getClientMetaDataFromClientMetaDataUri(json.clientMetadataUri) json.clientMetaDetails = resolvedClientMetaDetails } - return json + return validatePresentationRequest(WrappedPresentationRequest(presentationRequest = json) , data) } else { return null } } - private suspend fun getPresentationDefinitionFromDefinitionUri(presentationDefinitionUri:String?):PresentationDefinition?{ + private fun validatePresentationRequest( + presentationRequest: WrappedPresentationRequest, + responseString: String? + ): WrappedPresentationRequest? { + if (presentationRequest.presentationRequest?.clientIdScheme == "x509_san_dns" && responseString != null) { + var x5cChain: List? = null + + x5cChain = X509SanRequestVerifier.instance.extractX5cFromJWT(responseString) + + // Calling the function + if (x5cChain != null) { + val isClientIdInDnsNames = X509SanRequestVerifier.instance.validateClientIDInCertificate( + x5cChain, + presentationRequest.presentationRequest?.clientId + ) + + val isSignatureValid = + X509SanRequestVerifier.instance.validateSignatureWithCertificate( + responseString, + x5cChain + ) + + val isTrustChainValid = + X509SanRequestVerifier.instance.validateTrustChain(x5cChain) + + return if(isClientIdInDnsNames && isSignatureValid && isTrustChainValid ) { + presentationRequest + } else{ + WrappedPresentationRequest(presentationRequest = null,errorResponse = ErrorResponse(error = null , errorDescription = "Invalid Request" )) + } + + } else { + return presentationRequest + } + + + } else { + return presentationRequest + } + + } + + private suspend fun processJwtFromRedirectUri(redirectUri: String): String? = + withContext(Dispatchers.IO) { + // Fetch JWT from the redirect URI + val jwt = fetchJwtFromUri(redirectUri) + jwt?.let { + // Decode the JWT to extract the payload + val decodedPayload = decodeJwtPayload(it) + decodedPayload?.let { payload -> + // Parse JSON payload and retrieve fields + val jsonPayload = JSONObject(payload) + val clientId = jsonPayload.optString("client_id") + val clientIdScheme = jsonPayload.optString("client_id_scheme") + + // Use client_id and client_id_scheme as needed + println("Client ID: $clientId") + println("Client ID Scheme: $clientIdScheme") + + return@withContext payload // Return payload or process further as needed + } + } + return@withContext null + } + + private fun fetchJwtFromUri(uri: String): String? { + try { + val url = URL(uri) + val connection = url.openConnection() as HttpURLConnection + connection.requestMethod = "GET" + + if (connection.responseCode == HttpURLConnection.HTTP_OK) { + val reader = BufferedReader(InputStreamReader(connection.inputStream)) + val jwt = reader.use { it.readText() } + connection.disconnect() + return jwt + } + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + + private fun decodeJwtPayload(jwt: String): String? { + val parts = jwt.split(".") + return if (parts.size == 3) { + val payload = parts[1] + val decodedBytes = Base64.decode(payload, Base64.URL_SAFE) + String(decodedBytes, StandardCharsets.UTF_8) + } else { + null + } + } + + private suspend fun getPresentationDefinitionFromDefinitionUri(presentationDefinitionUri: String?): PresentationDefinition? { if (presentationDefinitionUri.isNullOrBlank()) return null @@ -196,16 +314,16 @@ class VerificationService : VerificationServiceInterface { PresentationDefinition::class.java ) return json - }else{ - if (isValidJWT(responseString?:"")) { + } else { + if (isValidJWT(responseString ?: "")) { val json = gson.fromJson( - parseJWTForPayload(responseString?:"{}"), + parseJWTForPayload(responseString ?: "{}"), PresentationDefinition::class.java ) return json - }else{ + } else { val json = gson.fromJson( - responseString?:"{}", + responseString ?: "{}", PresentationDefinition::class.java ) return json @@ -216,7 +334,7 @@ class VerificationService : VerificationServiceInterface { } } - private suspend fun getClientMetaDataFromClientMetaDataUri(clientMetadataUri:String?):ClientMetaDetails?{ + private suspend fun getClientMetaDataFromClientMetaDataUri(clientMetadataUri: String?): ClientMetaDetails? { if (clientMetadataUri.isNullOrBlank()) return null @@ -233,23 +351,23 @@ class VerificationService : VerificationServiceInterface { ClientMetaDetails::class.java ) return json - }else{ - if (isValidJWT(responseString?:"")) { + } else { + if (isValidJWT(responseString ?: "")) { val json = gson.fromJson( - parseJWTForPayload(responseString?:"{}"), + parseJWTForPayload(responseString ?: "{}"), ClientMetaDetails::class.java ) return json - }else{ + } else { val json = gson.fromJson( - responseString?:"{}", + responseString ?: "{}", ClientMetaDetails::class.java ) return json } } } catch (e: Exception) { - return null + return null } } else { return null @@ -338,27 +456,29 @@ class VerificationService : VerificationServiceInterface { credentialList: List ): WrappedVpTokenResponse? { - val presentationDefinition= processPresentationDefinition(presentationRequest.presentationDefinition) + val presentationDefinition = + processPresentationDefinition(presentationRequest.presentationDefinition) val formatMap = presentationDefinition.format?.takeIf { it.isNotEmpty() } ?: presentationDefinition.inputDescriptors ?.flatMap { it.format?.toList() ?: emptyList() } ?.toMap() val vpToken = if (formatMap?.containsKey("mso_mdoc") == true) { - mdocVpToken(credentialList,presentationRequest) + mdocVpToken(credentialList, presentationRequest) } else { - vpToken(presentationRequest, did, credentialList,subJwk ) + vpToken(presentationRequest, did, credentialList, subJwk) } - val presentationSubmission = if (formatMap?.containsKey("mso_mdoc") == true){ + val presentationSubmission = if (formatMap?.containsKey("mso_mdoc") == true) { createMdocPresentationSubmission( presentationRequest ) - }else{ + } else { createPresentationSubmission( presentationRequest ) } + val response = ApiManager.api.getService()?.sendVPToken( presentationRequest.responseUri ?: presentationRequest.redirectUri ?: "", mapOf( @@ -371,7 +491,7 @@ class VerificationService : VerificationServiceInterface { ) val tokenResponse = when { - response?.code() == 200 ->{ + response?.code() == 200 -> { val redirectUri = response.body()?.string() val gson = Gson() try { @@ -379,17 +499,19 @@ class VerificationService : VerificationServiceInterface { gson.fromJson(redirectUri, VPTokenResponse::class.java) return WrappedVpTokenResponse( vpTokenResponse = VPTokenResponse( - location = vpTokenResponse.redirectUri ?: "https://www.example.com?code=1" + location = vpTokenResponse.redirectUri + ?: "https://www.example.com?code=1" ) ) } catch (e: Exception) { return WrappedVpTokenResponse( vpTokenResponse = VPTokenResponse( - location ="https://www.example.com?code=1" + location = "https://www.example.com?code=1" ) ) } } + response?.code() == 302 || response?.code() == 200 -> { val locationHeader = response.headers()["Location"] if (locationHeader?.contains("error=") == true) { @@ -479,14 +601,19 @@ class VerificationService : VerificationServiceInterface { jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk) else ECDSASigner(subJwk as ECKey)) return jwt.serialize() } - private fun mdocVpToken(credentialList: List, presentationRequest: PresentationRequest): String { + + private fun mdocVpToken( + credentialList: List, + presentationRequest: PresentationRequest + ): String { // Validate input if (credentialList.isEmpty()) { throw IllegalArgumentException("Credential list cannot be empty") } return try { // Extract presentation definition once, as it doesn't change for different credentials - val presentationDefinition = VerificationService().processPresentationDefinition(presentationRequest.presentationDefinition) + val presentationDefinition = + VerificationService().processPresentationDefinition(presentationRequest.presentationDefinition) // Create a list to hold documents val documentList = mutableListOf() @@ -496,7 +623,8 @@ class VerificationService : VerificationServiceInterface { // Extract issuer authentication, docType, and namespaces for each credential val issuerAuth = CborUtils.processExtractIssuerAuth(listOf(credential)) val docType = CborUtils.extractDocTypeFromIssuerAuth(listOf(credential)) - val nameSpaces = CborUtils.processExtractNameSpaces(listOf(credential), presentationRequest) + val nameSpaces = + CborUtils.processExtractNameSpaces(listOf(credential), presentationRequest) // Create IssuerSigned object for this credential @@ -564,13 +692,17 @@ class VerificationService : VerificationServiceInterface { if (formatMap != null) { if (formatMap.containsKey("mso_mdoc")) { credentialList = ArrayList(allCredentialList) - processedCredentials = CborUtils.processMdocCredentialToJsonString(allCredentialList) ?: emptyList() + processedCredentials = + CborUtils.processMdocCredentialToJsonString(allCredentialList) + ?: emptyList() } else { - credentialList = splitCredentialsBySdJWT(allCredentialList, inputDescriptors.constraints?.limitDisclosure != null) + credentialList = splitCredentialsBySdJWT( + allCredentialList, + inputDescriptors.constraints?.limitDisclosure != null + ) processedCredentials = processCredentialsToJsonString(credentialList) } } - val filteredCredentialList: MutableList = mutableListOf() val inputDescriptor = Gson().toJson(inputDescriptors) @@ -588,7 +720,7 @@ class VerificationService : VerificationServiceInterface { } - private fun splitCredentialsBySdJWT( + fun splitCredentialsBySdJWT( allCredentials: List, isSdJwt: Boolean ): ArrayList { @@ -602,14 +734,15 @@ class VerificationService : VerificationServiceInterface { return filteredCredentials } - private fun processCredentialsToJsonString(credentialList: ArrayList):List{ + fun processCredentialsToJsonString(credentialList: ArrayList): List { var processedCredentials: List = mutableListOf() for (cred in credentialList) { val split = cred?.split(".") val jsonString = if ((cred?.split("~")?.size ?: 0) > 0) - SDJWTService().updateIssuerJwtWithDisclosuresForFiltering(cred) + //SDJWTService().updateIssuerJwtWithDisclosuresForFiltering(cred) + SDJWTService().updateIssuerJwtWithDisclosures(cred) else Base64.decode( split?.get(1) ?: "", @@ -674,9 +807,8 @@ class VerificationService : VerificationServiceInterface { id = inputDescriptors.id, path = "$", format = - presentationDefinition.format?.keys?.firstOrNull() ?: - inputDescriptors.format?.keys?.firstOrNull() ?: - "jwt_vp", + presentationDefinition.format?.keys?.firstOrNull() + ?: inputDescriptors.format?.keys?.firstOrNull() ?: "jwt_vp", pathNested = PathNested( id = inputDescriptors.id, format = "jwt_vc", @@ -693,6 +825,7 @@ class VerificationService : VerificationServiceInterface { ) return presentationSubmission } + private fun createMdocPresentationSubmission( presentationRequest: PresentationRequest ): PresentationSubmissionMdoc? { diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt index 419eddf..0502ad6 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/verification/VerificationServiceInterface.kt @@ -2,6 +2,7 @@ package com.ewc.eudi_wallet_oidc_android.services.verification import com.ewc.eudi_wallet_oidc_android.models.PresentationDefinition import com.ewc.eudi_wallet_oidc_android.models.PresentationRequest +import com.ewc.eudi_wallet_oidc_android.models.WrappedPresentationRequest import com.ewc.eudi_wallet_oidc_android.models.WrappedVpTokenResponse import com.github.decentraliseddataexchange.presentationexchangesdk.PresentationExchange import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential @@ -22,7 +23,7 @@ interface VerificationServiceInterface { * * @return PresentationRequest */ - suspend fun processAuthorisationRequest(data: String?): PresentationRequest? + suspend fun processAuthorisationRequest(data: String?): WrappedPresentationRequest? /** * Authorisation response is sent by constructing the vp_token and presentation_submission values.