From 8785222e7ab750d63b0f97574a37be6a27bff3d4 Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 9 Apr 2024 12:29:50 +0530 Subject: [PATCH 01/29] Fix #3: Handle model difference in Issuer config and Offer credential --- .../eudiwalletoidcandroid/MainViewModel.kt | 5 +- ...thorisationServerWellKnownConfiguration.kt | 1 - .../models/CredentialOffer.kt | 10 +++ .../models/IssuerWellKnownConfiguration.kt | 14 +-- .../services/issue/IssueService.kt | 87 +++++++++++++++++-- .../services/issue/IssueServiceInterface.kt | 25 +++++- 6 files changed, 124 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt index 7506175..4f0083c 100644 --- a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt +++ b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt @@ -119,6 +119,8 @@ class MainViewModel : ViewModel() { } private suspend fun getCredential() { + val types = IssueService().getTypesFromCredentialOffer(offerCredential) + val format = IssueService().getFormatFromIssuerConfig(issuerConfig, types.lastOrNull() ?:"") val credential = IssueService().processCredentialRequest( did, subJwk, @@ -126,7 +128,8 @@ class MainViewModel : ViewModel() { tokenResponse?.tokenResponse?.cNonce, offerCredential, issuerConfig?.credentialEndpoint, - tokenResponse?.tokenResponse?.accessToken + tokenResponse?.tokenResponse?.accessToken, + format?:"jwt_vc" ) withContext(Dispatchers.Main) { diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt index b476819..c70b506 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt @@ -14,7 +14,6 @@ data class AuthorisationServerWellKnownConfiguration( @SerializedName("response_modes_supported") var responseModesSupported: ArrayList = arrayListOf(), @SerializedName("grant_types_supported") var grantTypesSupported: ArrayList = arrayListOf(), @SerializedName("subject_types_supported") var subjectTypesSupported: ArrayList = arrayListOf(), -// @SerializedName("id_token_signing_alg_values_supported") var idTokenSigningAlgValuesSupported: ArrayList = arrayListOf(), @SerializedName("request_object_signing_alg_values_supported") var requestObjectSigningAlgValuesSupported: ArrayList = arrayListOf(), @SerializedName("request_parameter_supported") var requestParameterSupported: Boolean? = null, @SerializedName("request_uri_parameter_supported") var requestUriParameterSupported: Boolean? = null, diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialOffer.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialOffer.kt index cd1a671..129ed49 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialOffer.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialOffer.kt @@ -3,11 +3,21 @@ package com.ewc.eudi_wallet_oidc_android.models import com.google.gson.annotations.SerializedName data class CredentialOffer( + @SerializedName("credential_issuer") var credentialIssuer: String? = null, + @SerializedName("credentials") var credentials: ArrayList? = null, + @SerializedName("grants") var grants: Grants? = null +) +data class CredentialOfferV1( @SerializedName("credential_issuer") var credentialIssuer: String? = null, @SerializedName("credentials") var credentials: ArrayList? = null, @SerializedName("grants") var grants: Grants? = null +) +data class CredentialOfferV2( + @SerializedName("credential_issuer") var credentialIssuer: String? = null, + @SerializedName("credentials") var credentials: ArrayList? = null, + @SerializedName("grants") var grants: Grants? = null ) data class Credentials( diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt index ce764d1..a133fad 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt @@ -3,40 +3,34 @@ package com.ewc.eudi_wallet_oidc_android.models import com.google.gson.annotations.SerializedName data class IssuerWellKnownConfiguration( - @SerializedName("issuer") var issuer: String? = null, @SerializedName("credential_issuer") var credentialIssuer: String? = null, @SerializedName("authorization_server") var authorizationServer: String? = null, + @SerializedName("authorization_servers") var authorizationServers: ArrayList? = null, @SerializedName("credential_endpoint") var credentialEndpoint: String? = null, @SerializedName("deferred_credential_endpoint") var deferredCredentialEndpoint: String? = null, - @SerializedName("display") var display: Display? = null, - @SerializedName("credentials_supported") var credentialsSupported: ArrayList = arrayListOf() - + @SerializedName("display") var display: Any? = null, + @SerializedName("credentials_supported") var credentialsSupported: Any? = null ) data class CredentialsSupported( - @SerializedName("format") var format: String? = null, @SerializedName("types") var types: ArrayList = arrayListOf(), @SerializedName("trust_framework") var trustFramework: TrustFramework? = TrustFramework(), @SerializedName("display") var display: ArrayList = arrayListOf() - ) data class Display( - @SerializedName("name") var name: String? = null, @SerializedName("location") var location: String? = null, @SerializedName("locale") var locale: String? = null, @SerializedName("cover") var cover: Image? = Image(), @SerializedName("logo") var logo: Image? = Image(), @SerializedName("description") var description: String? = null - ) data class Image( - + @SerializedName("uri") var uri: String? = null, @SerializedName("url") var url: String? = null, @SerializedName("alt_text") var altText: String? = null - ) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index 62e0dfd..0ad3634 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -4,11 +4,13 @@ import android.net.Uri import com.ewc.eudi_wallet_oidc_android.models.AuthorizationDetails import com.ewc.eudi_wallet_oidc_android.models.ClientMetaData import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer +import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV1 +import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV2 import com.ewc.eudi_wallet_oidc_android.models.CredentialRequest import com.ewc.eudi_wallet_oidc_android.models.ErrorResponse +import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.Jwt import com.ewc.eudi_wallet_oidc_android.models.ProofV3 -import com.ewc.eudi_wallet_oidc_android.models.TokenResponse import com.ewc.eudi_wallet_oidc_android.models.VpFormatsSupported import com.ewc.eudi_wallet_oidc_android.models.WrappedCredentialResponse import com.ewc.eudi_wallet_oidc_android.models.WrappedTokenResponse @@ -84,7 +86,7 @@ class IssueService : IssueServiceInterface { val authorisationDetails = Gson().toJson( arrayListOf( AuthorizationDetails( - types = credentialOffer?.credentials?.get(0)?.types, + types = getTypesFromCredentialOffer(credentialOffer), locations = arrayListOf(credentialOffer?.credentialIssuer ?: "") ) ) @@ -267,7 +269,8 @@ class IssueService : IssueServiceInterface { nonce: String?, credentialOffer: CredentialOffer?, credentialIssuerEndPoint: String?, - accessToken: String? + accessToken: String?, + format:String ): WrappedCredentialResponse? { // Add claims @@ -296,8 +299,8 @@ class IssueService : IssueServiceInterface { // Construct credential request val body = CredentialRequest( - types = credentialOffer?.credentials?.get(0)?.types, - format = credentialOffer?.credentials?.get(0)?.format, + types = getTypesFromCredentialOffer(credentialOffer), + format = format, ProofV3( proofType = "jwt", jwt = jwt.serialize() @@ -413,5 +416,79 @@ class IssueService : IssueServiceInterface { } } + /** + * Get format from IssuerWellKnownConfiguration + * + * @param issuerConfig + * @param type + */ + override fun getFormatFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): String? { + var format: String = "jwt_vc" + val credentialOfferJsonString = Gson().toJson(issuerConfig) + + val jsonObject = JSONObject(credentialOfferJsonString) + + val credentialsSupported: Any = jsonObject.opt("credentials_supported") ?: return null + + when (credentialsSupported) { + is JSONObject -> { + val credentialSupported = credentialsSupported.getJSONObject(type ?: "") + format = credentialSupported.getString("format") + } + + is JSONArray -> { + for (i in 0 until credentialsSupported.length()) { + val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) + + // Get the "types" JSONArray + val typesArray = jsonObject.getJSONArray("types") + + // Check if the string is present in the "types" array + for (j in 0 until typesArray.length()) { + if (typesArray.getString(j) == type) { + format = jsonObject.getString("format") + break + } + } + } + } + + else -> { + // Neither JSONObject nor JSONArray + println("Child is neither JSONObject nor JSONArray") + } + } + + return format + } + + /** + * Get types from credential offer + * + * @param credentialOffer + * @return + */ + override fun getTypesFromCredentialOffer(credentialOffer: CredentialOffer?): ArrayList { + var types: ArrayList = ArrayList() + val credentialOfferJsonString = Gson().toJson(credentialOffer) + try { + try { + val credentialOfferV2 = + Gson().fromJson(credentialOfferJsonString, CredentialOfferV2::class.java) + types = credentialOfferV2.credentials ?: ArrayList() + } catch (e: Exception) { + val credentOfferV1 = + Gson().fromJson(credentialOfferJsonString, CredentialOfferV1::class.java) + types = credentOfferV1?.credentials?.get(0)?.types ?: ArrayList() + } + } catch (e: Exception) { + + } + + return types + } } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt index 5d650bd..c49da64 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt @@ -1,6 +1,7 @@ package com.ewc.eudi_wallet_oidc_android.services.issue import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer +import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.WrappedCredentialResponse import com.ewc.eudi_wallet_oidc_android.models.WrappedTokenResponse import com.nimbusds.jose.jwk.ECKey @@ -83,7 +84,8 @@ interface IssueServiceInterface { nonce: String?, credentialOffer: CredentialOffer?, credentialIssuerEndPoint: String?, - accessToken: String? + accessToken: String?, + format: String ): WrappedCredentialResponse? /** @@ -97,4 +99,25 @@ interface IssueServiceInterface { acceptanceToken: String?, deferredCredentialEndPoint: String? ): WrappedCredentialResponse? + + /** + * Get format from IssuerWellKnownConfiguration + * + * @param issuerConfig + * @param type + */ + fun getFormatFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): String? + + /** + * Get types from credential offer + * + * @param credentialOffer + * @return + */ + fun getTypesFromCredentialOffer( + credentialOffer: CredentialOffer? + ): ArrayList } \ No newline at end of file From ec2ac6f10769a3a942b535e7e2a29596d7f2438f Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 9 Apr 2024 16:26:38 +0530 Subject: [PATCH 02/29] Fix #18: Issue on digest and disclosure order mismatch --- .../services/sdjwt/SDJWTService.kt | 18 +++++++++++++++--- .../verification/VerificationService.kt | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt index 6ac6b00..529cf90 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt @@ -7,6 +7,7 @@ import com.ewc.eudi_wallet_oidc_android.models.PresentationRequest import com.ewc.eudi_wallet_oidc_android.services.verification.VerificationService import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential import com.google.gson.Gson +import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonParser @@ -155,7 +156,8 @@ class SDJWTService : SDJWTServiceInterface { val jsonObject = Gson().fromJson(jsonString, JsonObject::class.java) val hashList: MutableList = mutableListOf() - val disclosures = getDisclosuresFromSDJWT(credential) + var disclosures = getDisclosuresFromSDJWT(credential) + disclosures = disclosures?.filter { it != null && it.isNotBlank() } disclosures?.forEach { encodedString -> try { val hash = calculateSHA256Hash(encodedString) @@ -185,8 +187,8 @@ class SDJWTService : SDJWTServiceInterface { val sdList = jsonObject.getAsJsonArray("_sd") hashList.forEachIndexed { index, hash -> - val sdKey = sdList[index].asString - if (hash == sdKey) { + + if (isStringPresentInJSONArray(sdList, hash)) { try { val disclosure = Base64.decode( disclosures[index], @@ -212,6 +214,16 @@ class SDJWTService : SDJWTServiceInterface { } } + private fun isStringPresentInJSONArray(jsonArray: JsonArray, searchString: String): Boolean { + for (i in 0 until jsonArray.size()) { + val element = jsonArray.elementAt(i).asString + if (element == searchString) { + return true + } + } + return false + } + private fun extractKeyValue(decodedString: String): Pair { val jsonArray = JsonParser.parseString(decodedString).asJsonArray val key = jsonArray[1].asString 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 46a5de1..1bc72a2 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 @@ -281,7 +281,7 @@ class VerificationService : VerificationServiceInterface { val descriptor = DescriptorMap( id = inputDescriptors.id, path = "$", - format = "jwt_vp", + format = presentationDefinition.format?.keys?.first(), pathNested = PathNested( id = inputDescriptors.id, format = "jwt_vc", From 3b3a0bc42147be891e6855d5239b085a237d834a Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 9 Apr 2024 16:35:07 +0530 Subject: [PATCH 03/29] Fix : Release 2024.4.1 --- eudi-wallet-oidc-android/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eudi-wallet-oidc-android/build.gradle.kts b/eudi-wallet-oidc-android/build.gradle.kts index 943812a..08f35ec 100644 --- a/eudi-wallet-oidc-android/build.gradle.kts +++ b/eudi-wallet-oidc-android/build.gradle.kts @@ -72,7 +72,7 @@ publishing { register("release") { groupId = "com.github.decentraliseddataexchange" artifactId = "eudi-wallet-oidc-android" - version = "2024.3.1" + version = "2024.4.1" afterEvaluate { from(components["release"]) From 93dfc16d1ba076b3347988f4b3b19902a5753e26 Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 11 Apr 2024 09:44:24 +0530 Subject: [PATCH 04/29] Fix #20: Issue in parsing JWT presentation request --- .../verification/VerificationService.kt | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) 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 1bc72a2..b1c8f4f 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 @@ -69,25 +69,22 @@ class VerificationService : VerificationServiceInterface { requestUri = requestUri, responseUri = responseUri ) - } else if (data.startsWith("openid4vp") - && !requestUri.isNullOrBlank() - ) { + } else if (!requestUri.isNullOrBlank() || !responseUri.isNullOrBlank()) { val response = - ApiManager.api.getService()?.getPresentationDefinitionFromRequestUri(requestUri) + ApiManager.api.getService() + ?.getPresentationDefinitionFromRequestUri(requestUri ?: responseUri ?: "") if (response?.isSuccessful == true) { - val split = response.body().toString().split(".")[1] + if (isValidJWT(response.body().toString())) { - val jsonString = Base64.decode( - split, - Base64.URL_SAFE - ).toString(charset("UTF-8")) - - val json = Gson().fromJson( - jsonString, - PresentationRequest::class.java - ) + val json = Gson().fromJson( + parseJWTForPayload(response.body().toString()), + PresentationRequest::class.java + ) - return json + return json + }else{ + return null + } } else { return null } @@ -108,14 +105,23 @@ class VerificationService : VerificationServiceInterface { private fun isValidJWT(token: String): Boolean { try { // Parse the JWT token - val parsedJWT: JWT = JWTParser.parse(token) - return parsedJWT.jwtClaimsSet != null - } catch (e: ParseException) { + val parsedJWT = SignedJWT.parse(token) + return parsedJWT.payload != null + } catch (e: Exception) { println("JWT parsing failed: ${e.message}") return false } } + @Throws(ParseException::class) + private fun parseJWTForPayload(accessToken: String): String { + try { + val decodedJWT = SignedJWT.parse(accessToken) + return decodedJWT.payload.toString() + } catch (e: ParseException) { + throw java.lang.Exception("Invalid token!") + } + } /** * Authorisation response is sent by constructing the vp_token and presentation_submission values. @@ -165,7 +171,7 @@ class VerificationService : VerificationServiceInterface { jwt.sign(ECDSASigner(subJwk)) val response = ApiManager.api.getService()?.sendVPToken( - presentationRequest.responseUri?:presentationRequest.redirectUri ?: "", + presentationRequest.responseUri ?: presentationRequest.redirectUri ?: "", mapOf( "vp_token" to jwt.serialize(), "presentation_submission" to Gson().toJson( @@ -207,7 +213,7 @@ class VerificationService : VerificationServiceInterface { Base64.URL_SAFE ).toString(charset("UTF-8")) - val json = JSONObject(jsonString?:"{}") + val json = JSONObject(jsonString ?: "{}") // todo known item, we are considering the path from only vc processedCredentials = From 36d219325b0e7e0e65cf5e6b8bd6d97f3793a0d2 Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 11 Apr 2024 09:45:50 +0530 Subject: [PATCH 05/29] Fix : Version update to 2024.4.2 --- eudi-wallet-oidc-android/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eudi-wallet-oidc-android/build.gradle.kts b/eudi-wallet-oidc-android/build.gradle.kts index 08f35ec..3a825e3 100644 --- a/eudi-wallet-oidc-android/build.gradle.kts +++ b/eudi-wallet-oidc-android/build.gradle.kts @@ -72,7 +72,7 @@ publishing { register("release") { groupId = "com.github.decentraliseddataexchange" artifactId = "eudi-wallet-oidc-android" - version = "2024.4.1" + version = "2024.4.2" afterEvaluate { from(components["release"]) From c9afda2030bf9fe7c70529a6ccaf753d1cc6556e Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 9 May 2024 15:36:44 +0530 Subject: [PATCH 06/29] Fix #22: Add KB JWT when it is requested in the presentation request --- .../services/sdjwt/SDJWTService.kt | 53 +++++++++++-------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt index 529cf90..64d29fc 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt @@ -65,37 +65,44 @@ class SDJWTService : SDJWTServiceInterface { subJwk: ECKey ): String? { try { + val presentationDefinition = + VerificationService().processPresentationDefinition(presentationRequest.presentationDefinition) val processedCredentialWithRequiredDisclosures = processDisclosuresWithPresentationDefinition( credential, - VerificationService().processPresentationDefinition(presentationRequest.presentationDefinition) + presentationDefinition ) - val iat = Date() - - val claimsSet = JWTClaimsSet.Builder() - .audience(presentationRequest.clientId) - .issueTime(iat) - .claim("nonce", UUID.randomUUID().toString()) - .claim( - "sd_hash", - SDJWTService().calculateSHA256Hash(processedCredentialWithRequiredDisclosures) - ) - .build() + if (presentationDefinition.format?.containsKey("kb_jwt") == true) { + val iat = Date() - // Create JWT for ES256K alg - val jwsHeader = JWSHeader.Builder(JWSAlgorithm.ES256) - .type(JOSEObjectType("kb_jwt")) - .build() + val claimsSet = JWTClaimsSet.Builder() + .audience(presentationRequest.clientId) + .issueTime(iat) + .claim("nonce", UUID.randomUUID().toString()) + .claim( + "sd_hash", + SDJWTService().calculateSHA256Hash( + processedCredentialWithRequiredDisclosures + ) + ) + .build() - val jwt = SignedJWT( - jwsHeader, - claimsSet - ) + // Create JWT for ES256K alg + val jwsHeader = JWSHeader.Builder(JWSAlgorithm.ES256) + .type(JOSEObjectType("kb_jwt")) + .build() - // Sign with private EC key - jwt.sign(ECDSASigner(subJwk)) + val jwt = SignedJWT( + jwsHeader, + claimsSet + ) + + // Sign with private EC key + jwt.sign(ECDSASigner(subJwk)) + return "${processedCredentialWithRequiredDisclosures}~${jwt.serialize()}" + } - return jwt.serialize() + return processedCredentialWithRequiredDisclosures } catch (e: Exception) { throw IllegalArgumentException("Error creating SD-JWT-R", e) } From ee56ae75b128aed49ffd01f3795304f02a835a94 Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 9 May 2024 15:39:36 +0530 Subject: [PATCH 07/29] Fix #23: Support for Object and Array in disclosures --- .../services/sdjwt/SDJWTService.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt index 64d29fc..efbbf6d 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt @@ -1,11 +1,9 @@ package com.ewc.eudi_wallet_oidc_android.services.sdjwt import android.util.Base64 -import android.util.Log 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.services.verification.VerificationService -import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential import com.google.gson.Gson import com.google.gson.JsonArray import com.google.gson.JsonElement @@ -204,7 +202,17 @@ class SDJWTService : SDJWTServiceInterface { // Extract key-value pair from the encodedString val (decodedKey, decodedValue) = extractKeyValue(disclosure) // Add key-value pair to jsonObject - jsonObject.addProperty(decodedKey, decodedValue) + // Check if decodedValue is an object + if (decodedValue is JsonObject) { + // If it's an object, add it directly + jsonObject.add(decodedKey, decodedValue) + } else if (decodedValue is JsonArray) { + // If it's an object, add it directly + jsonObject.add(decodedKey, decodedValue) + } else { + // Otherwise, add it as a property + jsonObject.addProperty(decodedKey, decodedValue.toString()) + } } catch (e: IllegalArgumentException) { // Handle invalid base64-encoded strings } @@ -231,10 +239,10 @@ class SDJWTService : SDJWTServiceInterface { return false } - private fun extractKeyValue(decodedString: String): Pair { + private fun extractKeyValue(decodedString: String): Pair { val jsonArray = JsonParser.parseString(decodedString).asJsonArray val key = jsonArray[1].asString - val value = jsonArray[2].asString + val value = jsonArray[2] return Pair(key, value) } From ca4b78a06c1245a839c406e5fbcb50cb4ef3c373 Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 9 May 2024 15:42:30 +0530 Subject: [PATCH 08/29] Fix #24: filter logic updated with limited_disclosures parameter --- .../models/InputDescriptors.kt | 1 + .../services/issue/IssueService.kt | 32 +++++++++++-------- .../verification/VerificationService.kt | 19 +++++++++-- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt index 1908ac9..f0c10cd 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt @@ -11,6 +11,7 @@ data class InputDescriptors( data class Constraints( + @SerializedName("limit_disclosure") var limitDisclosure: String? = null, @SerializedName("fields") var fields: ArrayList? = null ) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index 0ad3634..de6f056 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -435,24 +435,30 @@ class IssueService : IssueServiceInterface { when (credentialsSupported) { is JSONObject -> { - val credentialSupported = credentialsSupported.getJSONObject(type ?: "") - format = credentialSupported.getString("format") + try { + val credentialSupported = credentialsSupported.getJSONObject(type ?: "") + format = credentialSupported.getString("format") + } catch (e: Exception) { + } } is JSONArray -> { - for (i in 0 until credentialsSupported.length()) { - val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) - - // Get the "types" JSONArray - val typesArray = jsonObject.getJSONArray("types") - - // Check if the string is present in the "types" array - for (j in 0 until typesArray.length()) { - if (typesArray.getString(j) == type) { - format = jsonObject.getString("format") - break + try { + for (i in 0 until credentialsSupported.length()) { + val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) + + // Get the "types" JSONArray + val typesArray = jsonObject.getJSONArray("types") + + // Check if the string is present in the "types" array + for (j in 0 until typesArray.length()) { + if (typesArray.getString(j) == type) { + format = jsonObject.getString("format") + break + } } } + } catch (e: Exception) { } } 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 b1c8f4f..13f03dc 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 @@ -82,7 +82,7 @@ class VerificationService : VerificationServiceInterface { ) return json - }else{ + } else { return null } } else { @@ -194,10 +194,25 @@ class VerificationService : VerificationServiceInterface { * Returns all the list of credentials matching for all input descriptors */ override suspend fun filterCredentials( - credentialList: List, + allCredentialList: List, presentationDefinition: PresentationDefinition ): List> { //list of credentials matched for all input descriptors + + val credentialList: ArrayList = arrayListOf() + for (item in allCredentialList) { + if (presentationDefinition.inputDescriptors?.get(0)?.constraints?.limitDisclosure != null && item?.contains( + "~" + ) == true + ) + credentialList.add(item) + else if (presentationDefinition.inputDescriptors?.get(0)?.constraints?.limitDisclosure == null && item?.contains( + "~" + ) != true + ) + credentialList.add(item) + } + val response: MutableList> = mutableListOf() var processedCredentials: List = mutableListOf() From f8539972853b2f2d2608d2622477b2d7c6ad92e5 Mon Sep 17 00:00:00 2001 From: milan Date: Thu, 9 May 2024 15:47:26 +0530 Subject: [PATCH 09/29] Fix #25: Version updated to 2024.5.1 --- eudi-wallet-oidc-android/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eudi-wallet-oidc-android/build.gradle.kts b/eudi-wallet-oidc-android/build.gradle.kts index 3a825e3..9f80c07 100644 --- a/eudi-wallet-oidc-android/build.gradle.kts +++ b/eudi-wallet-oidc-android/build.gradle.kts @@ -72,7 +72,7 @@ publishing { register("release") { groupId = "com.github.decentraliseddataexchange" artifactId = "eudi-wallet-oidc-android" - version = "2024.4.2" + version = "2024.5.1" afterEvaluate { from(components["release"]) From fd7400c7edcdd3da9e6260a5412f65f92728cd84 Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 28 May 2024 16:50:41 +0530 Subject: [PATCH 10/29] Fix #26: Support for EdDSA --- .../CryptographicAlgorithms.kt | 6 + .../models/CredentialRequest.kt | 5 + .../models/IssuerWellKnownConfiguration.kt | 3 +- .../services/did/DIDService.kt | 111 +++++- .../services/did/DIDServiceInterface.kt | 56 ++- .../services/issue/IssueService.kt | 369 +++++++++++++++++- .../services/issue/IssueServiceInterface.kt | 72 ++++ .../services/sdjwt/SDJWTService.kt | 61 +++ .../services/sdjwt/SDJWTServiceInterface.kt | 7 + .../verification/VerificationService.kt | 76 ++++ .../VerificationServiceInterface.kt | 18 + 11 files changed, 772 insertions(+), 12 deletions(-) create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/CryptographicAlgorithms.kt diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/CryptographicAlgorithms.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/CryptographicAlgorithms.kt new file mode 100644 index 0000000..ce961f1 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/CryptographicAlgorithms.kt @@ -0,0 +1,6 @@ +package com.ewc.eudi_wallet_oidc_android + +object CryptographicAlgorithms { + final val ES256 = "ES256" + final val EdDSA = "EdDSA" +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt index dce9c1b..4da2222 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt @@ -6,11 +6,16 @@ import com.google.gson.annotations.SerializedName data class CredentialRequest( @SerializedName("types") var types: ArrayList? = null, + @SerializedName("credential_definition") var credentialDefinition: CredentialDefinition? = null, @SerializedName("format") var format: String? = null, @SerializedName("proof") var proof: ProofV3? = null ) +data class CredentialDefinition( + @SerializedName("vct") var vct: String? = null, + @SerializedName("type") var type: ArrayList? = null +) data class ProofV3( @SerializedName("proof_type") var proofType: String? = null, diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt index a133fad..3e66f78 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt @@ -17,7 +17,8 @@ data class CredentialsSupported( @SerializedName("format") var format: String? = null, @SerializedName("types") var types: ArrayList = arrayListOf(), @SerializedName("trust_framework") var trustFramework: TrustFramework? = TrustFramework(), - @SerializedName("display") var display: ArrayList = arrayListOf() + @SerializedName("display") var display: ArrayList = arrayListOf(), + @SerializedName("cryptographic_suites_supported") var cryptographicSuitesSupported: ArrayList = arrayListOf() ) data class Display( diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt index 066261a..37c4c2e 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt @@ -1,8 +1,10 @@ package com.ewc.eudi_wallet_oidc_android.services.did +import com.ewc.eudi_wallet_oidc_android.CryptographicAlgorithms import com.mediaparkpk.base58android.Base58 import com.nimbusds.jose.jwk.Curve import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator import com.nimbusds.jose.util.Base64URL @@ -73,11 +75,112 @@ class DIDService : DIDServiceInterface { } /** - * Generate JWK of curve Ed25519 + * Create DID according to cryptographicAlgorithm * - * @return JWK + * @param jwk + * @param cryptographicAlgorithm + * @return */ - override fun createED25519JWK(): OctetKeyPair? { + override fun createDID(jwk: JWK, cryptographicAlgorithm: String?): String { + when (cryptographicAlgorithm) { + CryptographicAlgorithms.ES256 -> { + return createES256DID(jwk) + } + + CryptographicAlgorithms.EdDSA -> { + return createEdDSADID((jwk as OctetKeyPair).x) + } + + else -> { + return createES256DID(jwk) + } + } + } + + /** + * Create JWK according to cryptographicAlgorithm + * + * @param seed + * @param cryptographicAlgorithm + * @return + */ + override fun createJWK(seed: String?, cryptographicAlgorithm: String?): JWK { + when (cryptographicAlgorithm) { + CryptographicAlgorithms.ES256 -> { + return createES256JWK(seed) + } + + CryptographicAlgorithms.EdDSA -> { + return createEdDSAJWK(seed) + } + + else -> { + return createES256JWK(seed) + } + } + } + + + /** + * Create ES256 JWK + * + * @param seed + * @return + */ + override fun createES256JWK(seed: String?): JWK { + val keyPairGenerator = KeyPairGenerator.getInstance("EC") + if (seed != null) { + val seedBytes = seed.toByteArray(StandardCharsets.UTF_8) + keyPairGenerator.initialize(256, SecureRandom(seedBytes)) + } else { + keyPairGenerator.initialize(256) + } + val keyPair: KeyPair = keyPairGenerator.generateKeyPair() + + val publicKey = convertToECPublicKey(keyPair.public) + val privateKey = convertToECPrivateKey(keyPair.private) + + val ecKey = ECKey.Builder(Curve.P_256, publicKey) + .privateKey(privateKey).build() + + return ecKey + } + + + /** + * Create ES256 DID + * + * @param jwk + * @return + */ + override fun createES256DID(jwk: JWK): String { + val ecKey = jwk as ECKey + val publicKey = ecKey.toPublicJWK() + + val compactJson = + "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"${publicKey?.x}\",\"y\":\"${publicKey?.y}\"}" + + // UTF-8 encode the string + val encodedBytes: ByteArray? = compactJson.toByteArray(StandardCharsets.UTF_8) + + // Add multiCodec byte + val multiCodecBytes = addMultiCodecByte(encodedBytes) + + // Apply multiBase base58-btc encoding + val multiBaseEncoded = multiBaseEncode(multiCodecBytes!!) + + // Prefix the string with "did:key" + return "did:key:z$multiBaseEncoded" + } + + + /** + * Create ED25519 JWK + * + * @param seed + * @return + */ + override fun createEdDSAJWK(seed: String?): JWK { val jwk = OctetKeyPairGenerator(Curve.Ed25519) .keyID(UUID.randomUUID().toString()) .generate() @@ -91,7 +194,7 @@ class DIDService : DIDServiceInterface { * * @return DID */ - override fun createDidED25519(privateKeyX: Base64URL): String { + override fun createEdDSADID(privateKeyX: Base64URL): String { val startArray = byteArrayOf(0xed.toByte(), 0x01) val newArray = startArray + Base64URL(privateKeyX.toString()).decode() // 3. base58 encode the prefixed public key bytes. diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt index b40dc01..7963283 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt @@ -1,6 +1,8 @@ package com.ewc.eudi_wallet_oidc_android.services.did +import com.ewc.eudi_wallet_oidc_android.CryptographicAlgorithms import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.util.Base64URL @@ -22,7 +24,57 @@ interface DIDServiceInterface { */ fun createJWK(seed: String? = null): ECKey - fun createED25519JWK(): OctetKeyPair? + /** + * Create DID according to cryptographicAlgorithm + * + * @param jwk + * @param cryptographicAlgorithm + * @return + */ + fun createDID( + jwk: JWK, + cryptographicAlgorithm: String? = CryptographicAlgorithms.ES256): String + + /** + * Create JWK according to cryptographicAlgorithm + * + * @param seed + * @param cryptographicAlgorithm + * @return + */ + fun createJWK( + seed: String? = null, + cryptographicAlgorithm: String? = CryptographicAlgorithms.ES256): JWK + + /** + * Create ES256 JWK + * + * @param seed + * @return + */ + fun createES256JWK(seed: String?): JWK - fun createDidED25519(privateKeyX: Base64URL): String + /** + * Create ES256 DID + * + * @param jwk + * @return + */ + fun createES256DID(jwk: JWK): String + + /** + * Create ED25519 JWK + * + * @param seed + * @return + */ + fun createEdDSAJWK(seed: String?): JWK? + + /** + * Generate DID for the ED25519 + * @param privateKeyX - X value of the ED25519 jwk + * + * @return DID + */ + fun createEdDSADID(privateKeyX: Base64URL): String } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index de6f056..7a691fb 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -1,8 +1,10 @@ package com.ewc.eudi_wallet_oidc_android.services.issue import android.net.Uri +import android.util.Log import com.ewc.eudi_wallet_oidc_android.models.AuthorizationDetails import com.ewc.eudi_wallet_oidc_android.models.ClientMetaData +import com.ewc.eudi_wallet_oidc_android.models.CredentialDefinition import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV1 import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV2 @@ -21,7 +23,10 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner +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.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import org.json.JSONArray @@ -142,11 +147,92 @@ class IssueService : IssueServiceInterface { } } + /** + * To process the authorisation request The authorisation request is to + * grant access to the credential endpoint + * + * @param did - DID created for the issuance + * @param subJwk - for singing the requests + * @param credentialOffer - To build the authorisation request + * @param codeVerifier - to build the authorisation request + * @param authorisationEndPoint - to build the authorisation request + * @return String - short-lived authorisation code + */ + override suspend fun processAuthorisationRequest( + did: String?, + subJwk: JWK?, + credentialOffer: CredentialOffer?, + codeVerifier: String, + authorisationEndPoint: String? + ): String? { + val responseType = "code" + val scope = "openid" + val state = UUID.randomUUID().toString() + val clientId = did + val authorisationDetails = Gson().toJson( + arrayListOf( + AuthorizationDetails( + types = getTypesFromCredentialOffer(credentialOffer), + locations = arrayListOf(credentialOffer?.credentialIssuer ?: "") + ) + ) + ) + + val redirectUri = "http://localhost:8080" + val nonce = UUID.randomUUID().toString() + + val codeChallenge = CodeVerifierService().generateCodeChallenge(codeVerifier) + val codeChallengeMethod = "S256" + val clientMetadata = Gson().toJson( + ClientMetaData( + vpFormatsSupported = VpFormatsSupported( + jwtVp = Jwt(arrayListOf("ES256")), jwtVc = Jwt(arrayListOf("ES256")) + ), responseTypesSupported = arrayListOf( + "vp_token", "id_token" + ), authorizationEndpoint = redirectUri + ) + ) + + val response = ApiManager.api.getService()?.processAuthorisationRequest( + authorisationEndPoint ?: "", + mapOf( + "response_type" to responseType, + "scope" to scope, + "state" to state, + "client_id" to (clientId ?: ""), + "authorization_details" to authorisationDetails, + "redirect_uri" to redirectUri, + "nonce" to nonce, + "code_challenge" to (codeChallenge ?: ""), + "code_challenge_method" to codeChallengeMethod, + "client_metadata" to clientMetadata, + "issuer_state" to (credentialOffer?.grants?.authorizationCode?.issuerState ?: "") + ), + ) + + val location: String? = if (response?.code() == 302) { + response.headers()["Location"] + } else { + null + } + + return if (Uri.parse(location).getQueryParameter("code") != null) { + location + } else { + processAuthorisationRequestUsingIdToken( + did = did, + authorisationEndPoint = authorisationEndPoint, + location = location, + subJwk = subJwk + ) + } + } + private suspend fun processAuthorisationRequestUsingIdToken( did: String?, authorisationEndPoint: String?, location: String?, - subJwk: ECKey? + subJwk: JWK? ): String? { val claimsSet = JWTClaimsSet.Builder() @@ -159,15 +245,17 @@ class IssueService : IssueServiceInterface { .build() // Create JWT for ES256K alg - val jwsHeader = JWSHeader.Builder(JWSAlgorithm.ES256).type(JOSEObjectType.JWT) - .keyID("$did#${did?.replace("did:key:", "")}").jwk(subJwk?.toPublicJWK()).build() + val jwsHeader = JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType.JWT) + .keyID("$did#${did?.replace("did:key:", "")}") + .build() val jwt = SignedJWT( jwsHeader, claimsSet ) // Sign with private EC key - jwt.sign(ECDSASigner(subJwk)) + jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner(subJwk as ECKey)) val response = ApiManager.api.getService()?.sendIdTokenForCode( url = Uri.parse(location).getQueryParameter("redirect_uri") ?: "", @@ -301,7 +389,7 @@ class IssueService : IssueServiceInterface { val body = CredentialRequest( types = getTypesFromCredentialOffer(credentialOffer), format = format, - ProofV3( + proof = ProofV3( proofType = "jwt", jwt = jwt.serialize() ) @@ -339,6 +427,144 @@ class IssueService : IssueServiceInterface { return credentialResponse } + /** + * To process the credential, credentials can be issued in two ways, + * intime and deferred + * + * If its intime, then we will receive the credential as the response + * If its deferred, then we will get he acceptance token and use this acceptance token to call deferred + * + * @param did + * @param subJwk + * @param nonce + * @param credentialOffer + * @param issuerConfig + * @param accessToken + * @param format + * + * @return credential response + */ + override suspend fun processCredentialRequest( + did: String?, + subJwk: JWK?, + nonce: String?, + credentialOffer: CredentialOffer?, + issuerConfig: IssuerWellKnownConfiguration?, + accessToken: String?, + format: String + ): WrappedCredentialResponse? { + + // Add claims + val claimsSet = JWTClaimsSet + .Builder() + .issueTime(Date()) + .expirationTime(Date(Date().time + 86400)) + .issuer(did) + .audience(issuerConfig?.credentialIssuer ?: "") + .claim("nonce", nonce).build() + + // Add header + val jwsHeader = JWSHeader + .Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType("openid4vci-proof+jwt")) + .keyID("$did#${did?.replace("did:key:", "")}") +// .jwk(subJwk?.toPublicJWK()) + .build() + + + // Sign with private EC key + val jwt = SignedJWT( + jwsHeader, claimsSet + ) + jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner(subJwk as ECKey)) + + // Construct credential request + val body = buildCredentialRequest( + credentialOffer = credentialOffer, + issuerConfig = issuerConfig, + format = format, + jwt = jwt.serialize() + ) + // API call + val response = ApiManager.api.getService()?.getCredential( + issuerConfig?.credentialEndpoint ?: "", + "application/json", + "Bearer $accessToken", + body + ) + + val credentialResponse = when { + response?.isSuccessful == true -> { + WrappedCredentialResponse( + credentialResponse = response.body() + ) + } + + (response?.code() ?: 0) >= 400 -> { + try { + WrappedCredentialResponse( + errorResponse = processError(response?.errorBody()?.string()) + ) + } catch (e: Exception) { + null + } + } + + else -> { + null + } + } + + return credentialResponse + } + + private fun buildCredentialRequest( + credentialOffer: CredentialOffer?, + issuerConfig: IssuerWellKnownConfiguration?, + format: String?, + jwt: String + ): CredentialRequest { + + val gson = Gson() + var credentialDefinitionNeeded = false + try { + val credentialOfferV1 = + gson.fromJson(gson.toJson(credentialOffer), CredentialOfferV1::class.java) + + if (credentialOfferV1?.credentials?.get(0)?.trustFramework == null) + credentialDefinitionNeeded = true + + } catch (e: Exception) { + credentialDefinitionNeeded = true + } + + if (credentialDefinitionNeeded) { + var types: ArrayList? = getTypesFromCredentialOffer(credentialOffer) + try { + types = getTypesFromIssuerConfig(issuerConfig, type = types?.last()) + } catch (e: Exception) { + + } + + return CredentialRequest( + credentialDefinition = CredentialDefinition(type = types), + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } else { + return CredentialRequest( + types = getTypesFromCredentialOffer(credentialOffer), + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } + } fun processError(err: String?): ErrorResponse? { // Known possibilities for error: @@ -471,6 +697,73 @@ class IssueService : IssueServiceInterface { return format } + /** + * Get types from IssuerWellKnownConfiguration + * + * @param issuerConfig + * @param type + */ + override fun getTypesFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): ArrayList? { + var types: ArrayList = ArrayList() + val credentialOfferJsonString = Gson().toJson(issuerConfig) + val jsonObject = JSONObject(credentialOfferJsonString) + + val credentialsSupported: Any = jsonObject.opt("credentials_supported") ?: return null + when (credentialsSupported) { + is JSONObject -> { + try { + val credentialSupported = credentialsSupported.getJSONObject(type ?: "") + val typeFromCredentialIssuer = + credentialSupported.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + } catch (e: Exception) { + } + } + + is JSONArray -> { + try { + for (i in 0 until credentialsSupported.length()) { + val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) + + // Get the "types" JSONArray + val typesArray = jsonObject.getJSONArray("types") + + // Check if the string is present in the "types" array + for (j in 0 until typesArray.length()) { + if (typesArray.getString(j) == type) { + val typeFromCredentialIssuer = + jsonObject.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + break + } + } + } + } catch (e: Exception) { + } + } + + else -> { + // Neither JSONObject nor JSONArray + println("Child is neither JSONObject nor JSONArray") + } + } + + return types + } + /** * Get types from credential offer * @@ -497,4 +790,70 @@ class IssueService : IssueServiceInterface { return types } + + /** + * Get cryptographicSuits from issuer config + * + * @param issuerConfig + * @param type + * @return + */ + override fun getCryptoFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): ArrayList? { + var types: ArrayList = ArrayList() + val credentialOfferJsonString = Gson().toJson(issuerConfig) + val jsonObject = JSONObject(credentialOfferJsonString) + + val credentialsSupported: Any = jsonObject.opt("credentials_supported") ?: return null + when (credentialsSupported) { + is JSONObject -> { + try { + val credentialSupported = credentialsSupported.getJSONObject(type ?: "") + val cryptographicSuitsSupported = + credentialSupported.getJSONArray("cryptographic_suites_supported") + for (i in 0 until cryptographicSuitsSupported.length()) { + // Get each JSONObject from the JSONArray + val type: String = cryptographicSuitsSupported.getString(i) + types.add(type) + } + } catch (e: Exception) { + } + } + + is JSONArray -> { + try { + for (i in 0 until credentialsSupported.length()) { + val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) + + // Get the "types" JSONArray + val typesArray = jsonObject.getJSONArray("types") + + // Check if the string is present in the "types" array + for (j in 0 until typesArray.length()) { + if (typesArray.getString(j) == type) { + val cryptographicSuitsSupported = + jsonObject.getJSONArray("cryptographic_suites_supported") + for (i in 0 until cryptographicSuitsSupported.length()) { + // Get each JSONObject from the JSONArray + val type: String = cryptographicSuitsSupported.getString(i) + types.add(type) + } + break + } + } + } + } catch (e: Exception) { + } + } + + else -> { + // Neither JSONObject nor JSONArray + println("Child is neither JSONObject nor JSONArray") + } + } + + return types + } } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt index c49da64..dbf1868 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt @@ -5,6 +5,7 @@ import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.WrappedCredentialResponse import com.ewc.eudi_wallet_oidc_android.models.WrappedTokenResponse import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK interface IssueServiceInterface { @@ -36,6 +37,25 @@ interface IssueServiceInterface { authorisationEndPoint: String? ): String? + /** + * To process the authorisation request + * The authorisation request is to grant access to the credential endpoint + * @param did - DID created for the issuance + * @param subJwk - for singing the requests + * @param credentialOffer - To build the authorisation request + * @param codeVerifier - to build the authorisation request + * @param authorisationEndPoint - to build the authorisation request + * + * @return String - Uri with query parameter code with value short-lived authorisation code + */ + suspend fun processAuthorisationRequest( + did: String?, + subJwk: JWK?, + credentialOffer: CredentialOffer?, + codeVerifier: String, + authorisationEndPoint: String? + ): String? + /** * To process the token, * @@ -74,6 +94,7 @@ interface IssueServiceInterface { * @param credentialOffer * @param credentialIssuerEndPoint * @param accessToken + * @param format * * @return credential response */ @@ -88,6 +109,33 @@ interface IssueServiceInterface { format: String ): WrappedCredentialResponse? + /** + * To process the credential, credentials can be issued in two ways, + * intime and deferred + * + * If its intime, then we will receive the credential as the response + * If its deferred, then we will get he acceptance token and use this acceptance token to call deferred + * + * @param did + * @param subJwk + * @param nonce + * @param credentialOffer + * @param issuerConfig + * @param accessToken + * @param format + * + * @return credential response + */ + suspend fun processCredentialRequest( + did: String?, + subJwk: JWK?, + nonce: String?, + credentialOffer: CredentialOffer?, + issuerConfig: IssuerWellKnownConfiguration?, + accessToken: String?, + format: String + ): WrappedCredentialResponse? + /** * For issuance of the deferred credential. * @param acceptanceToken - token which we got from credential request @@ -120,4 +168,28 @@ interface IssueServiceInterface { fun getTypesFromCredentialOffer( credentialOffer: CredentialOffer? ): ArrayList + + /** + * Get types from Issuer Config + * + * @param issuerConfig + * @param type + * @return + */ + fun getTypesFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): ArrayList? + + /** + * Get types from Issuer Config + * + * @param issuerConfig + * @param type + * @return + */ + fun getCryptoFromIssuerConfig( + issuerConfig: IssuerWellKnownConfiguration?, + type: String? + ): ArrayList? } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt index efbbf6d..fdd844f 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt @@ -13,7 +13,10 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner +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.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import org.json.JSONArray @@ -106,6 +109,64 @@ class SDJWTService : SDJWTServiceInterface { } } + + /** + * Create SDJWT R + * + * @param credential + * @param presentationRequest + * @param subJwk + * @return + */ + override fun createSDJWTR( + credential: String?, + presentationRequest: PresentationRequest, + subJwk: JWK + ): String? { + try { + val presentationDefinition = + VerificationService().processPresentationDefinition(presentationRequest.presentationDefinition) + val processedCredentialWithRequiredDisclosures = + processDisclosuresWithPresentationDefinition( + credential, + presentationDefinition + ) + if (presentationDefinition.format?.containsKey("kb_jwt") == true) { + val iat = Date() + + val claimsSet = JWTClaimsSet.Builder() + .audience(presentationRequest.clientId) + .issueTime(iat) + .claim("nonce", UUID.randomUUID().toString()) + .claim( + "sd_hash", + SDJWTService().calculateSHA256Hash( + processedCredentialWithRequiredDisclosures + ) + ) + .build() + + // Create JWT for ES256K alg + val jwsHeader = JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType("kb_jwt")) + .build() + + val jwt = SignedJWT( + jwsHeader, + claimsSet + ) + + // Sign with private EC key + jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk) else ECDSASigner(subJwk as ECKey)) + return "${processedCredentialWithRequiredDisclosures}~${jwt.serialize()}" + } + + return processedCredentialWithRequiredDisclosures + } catch (e: Exception) { + throw IllegalArgumentException("Error creating SD-JWT-R", e) + } + } + /** * Processes disclosures based on the provided credential and presentation definition. * diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt index eb56ba5..86f56e0 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTServiceInterface.kt @@ -4,6 +4,7 @@ import com.ewc.eudi_wallet_oidc_android.models.PresentationDefinition import com.ewc.eudi_wallet_oidc_android.models.PresentationRequest import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK interface SDJWTServiceInterface { @@ -15,6 +16,12 @@ interface SDJWTServiceInterface { subJwk: ECKey ): String? + fun createSDJWTR( + credential: String?, + presentationRequest: PresentationRequest, + subJwk: JWK + ): String? + fun processDisclosuresWithPresentationDefinition( credential: String?, presentationDefinition: PresentationDefinition 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 13f03dc..2bdbe1a 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 @@ -17,7 +17,10 @@ import com.nimbusds.jose.JOSEObjectType import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner +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.JWT import com.nimbusds.jwt.JWTClaimsSet @@ -190,6 +193,79 @@ class VerificationService : VerificationServiceInterface { } } + /** + * Send VP token + * + * @param did + * @param subJwk + * @param presentationRequest + * @param credentialList + * @return + */ + override suspend fun sendVPToken( + did: String?, + subJwk: JWK?, + presentationRequest: PresentationRequest, + credentialList: List + ): String? { + val iat = Date() + val jti = "urn:uuid:${UUID.randomUUID()}" + val claimsSet = JWTClaimsSet.Builder() + .audience(presentationRequest.clientId) + .issueTime(iat) + .expirationTime(Date(iat.time + 600000)) + .issuer(did) + .jwtID(jti) + .notBeforeTime(iat) + .claim("nonce", presentationRequest.nonce) + .subject(did) + .claim( + "vp", com.nimbusds.jose.shaded.json.JSONObject( + hashMapOf( + "@context" to listOf("https://www.w3.org/2018/credentials/v1"), + "holder" to did, + "id" to jti, + "type" to listOf("VerifiablePresentation"), + "verifiableCredential" to credentialList + ) + ) + ).build() + + // Create JWT for ES256K alg + val jwsHeader = JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType("JWT")) + .keyID("$did#${did?.replace("did:key:", "")}") + .jwk(subJwk?.toPublicJWK()) + .build() + + val jwt = SignedJWT( + jwsHeader, + claimsSet + ) + + // Sign with private EC key + jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk) else ECDSASigner(subJwk as ECKey)) + + val response = ApiManager.api.getService()?.sendVPToken( + presentationRequest.responseUri ?: presentationRequest.redirectUri ?: "", + mapOf( + "vp_token" to jwt.serialize(), + "presentation_submission" to Gson().toJson( + createPresentationSubmission( + presentationRequest + ) + ), + "state" to (presentationRequest.state ?: "") + ) + ) + + return if (response?.code() == 302 || response?.code() == 200) { + response.headers()["Location"] ?: "https://tid-wallet-poc.azurewebsites.net?code=1" + } else { + null + } + } + /** * Returns all the list of credentials matching for all input descriptors */ 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 4ebce70..44061ce 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 @@ -7,6 +7,7 @@ import com.github.decentraliseddataexchange.presentationexchangesdk.models.Match import com.google.gson.Gson import com.google.gson.internal.LinkedTreeMap import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK interface VerificationServiceInterface { @@ -38,6 +39,23 @@ interface VerificationServiceInterface { credentialList: List ): String? + + /** + * Send VP token + * + * @param did + * @param subJwk + * @param presentationRequest + * @param credentialList + * @return + */ + suspend fun sendVPToken( + did: String?, + subJwk: JWK?, + presentationRequest: PresentationRequest, + credentialList: List + ): String? + /** * To filter the credential using the input descriptors * @param credentialList - list of all issued credentials From 99ac76de168834d0ae9ef40911074d4550118ba0 Mon Sep 17 00:00:00 2001 From: milan Date: Wed, 29 May 2024 10:40:35 +0530 Subject: [PATCH 11/29] Fix #27: Updated sample for EdDSA --- .../ewc/eudiwalletoidcandroid/MainActivity.kt | 16 +++---- .../eudiwalletoidcandroid/MainViewModel.kt | 42 +++++++++++++++---- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt index 91a218d..852dfc2 100644 --- a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt +++ b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt @@ -40,14 +40,14 @@ class MainActivity : AppCompatActivity() { } private fun initClicks() { - binding.btnCreateDID.setOnClickListener { - viewModel?.subJwk = DIDService().createJWK() - viewModel?.did = DIDService().createDID(viewModel?.subJwk!!) - - viewModel?.displayText?.value = "Sub JWK : \n ${Gson().toJson(viewModel?.subJwk)}\n\n" - viewModel?.displayText?.value = - "${viewModel?.displayText?.value}Did : ${viewModel?.did}\n\n" - } +// binding.btnCreateDID.setOnClickListener { +// viewModel?.subJwk = DIDService().createJWK() +// viewModel?.did = DIDService().createDID(viewModel?.subJwk!!) +// +// viewModel?.displayText?.value = "Sub JWK : \n ${Gson().toJson(viewModel?.subJwk)}\n\n" +// viewModel?.displayText?.value = +// "${viewModel?.displayText?.value}Did : ${viewModel?.did}\n\n" +// } binding.addCredential.setOnClickListener { if (ContextCompat.checkSelfPermission( diff --git a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt index 4f0083c..b16935b 100644 --- a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt +++ b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt @@ -3,6 +3,7 @@ package com.ewc.eudiwalletoidcandroid import android.net.Uri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.ewc.eudi_wallet_oidc_android.CryptographicAlgorithms import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration @@ -17,15 +18,19 @@ import com.ewc.eudi_wallet_oidc_android.services.sdjwt.SDJWTService import com.ewc.eudi_wallet_oidc_android.services.verification.VerificationService import com.google.gson.Gson import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.ArrayList import java.util.Timer import java.util.TimerTask class MainViewModel : ViewModel() { + private var format: String? = null + private var types: ArrayList = arrayListOf() var isLoading = MutableLiveData(false) var isPreAuthorised = MutableLiveData(false) @@ -40,7 +45,7 @@ class MainViewModel : ViewModel() { private var issuerConfig: IssuerWellKnownConfiguration? = null private var offerCredential: CredentialOffer? = null lateinit var did: String - lateinit var subJwk: ECKey + lateinit var subJwk: JWK init { isLoading.value = false @@ -58,15 +63,36 @@ class MainViewModel : ViewModel() { authConfig = DiscoveryService().getAuthConfig( - "${issuerConfig?.authorizationServer ?: issuerConfig?.issuer}/.well-known/openid-configuration" + "${issuerConfig?.authorizationServer + ?: issuerConfig?.authorizationServers?.get(0 + )?: issuerConfig?.issuer}/.well-known/openid-configuration" ) + types = IssueService().getTypesFromCredentialOffer(offerCredential) + format = + IssueService().getFormatFromIssuerConfig(issuerConfig, types.lastOrNull() ?: "") + val cryptoSupported = IssueService().getCryptoFromIssuerConfig( + issuerConfig, + types.lastOrNull() ?: "ES256" + ) + + subJwk = DIDService().createJWK( + null, + cryptoSupported?.lastOrNull() ?: CryptographicAlgorithms.ES256 + ) + did = DIDService().createDID( + subJwk, + cryptoSupported?.lastOrNull() ?: CryptographicAlgorithms.ES256 + ) // Generating code verifier codeVerifier = CodeVerifierService().generateCodeVerifier() withContext(Dispatchers.Main) { + displayText.value = "Sub JWK : \n ${Gson().toJson(subJwk)}\n\n" + displayText.value = + "${displayText.value}Did : $did\n\n" displayText.value = - "${displayText?.value}Issuer Config : \n${Gson().toJson(issuerConfig)}\n\n" + "${displayText.value}Issuer Config : \n${Gson().toJson(issuerConfig)}\n\n" displayText.value = "${displayText.value}Auth Config : \n${Gson().toJson(authConfig)}\n\n" displayText.value = @@ -95,7 +121,7 @@ class MainViewModel : ViewModel() { // Process Authorisation request val authResponse = IssueService().processAuthorisationRequest( did, - subJwk, + subJwk as ECKey, offerCredential, codeVerifier, authConfig?.authorizationEndpoint @@ -119,17 +145,15 @@ class MainViewModel : ViewModel() { } private suspend fun getCredential() { - val types = IssueService().getTypesFromCredentialOffer(offerCredential) - val format = IssueService().getFormatFromIssuerConfig(issuerConfig, types.lastOrNull() ?:"") + val credential = IssueService().processCredentialRequest( did, subJwk, - issuerConfig?.credentialIssuer, tokenResponse?.tokenResponse?.cNonce, offerCredential, - issuerConfig?.credentialEndpoint, + issuerConfig, tokenResponse?.tokenResponse?.accessToken, - format?:"jwt_vc" + format ?: "jwt_vc" ) withContext(Dispatchers.Main) { From f9e2821a814e819301d7e5a562cd638f2797d225 Mon Sep 17 00:00:00 2001 From: milan Date: Wed, 29 May 2024 13:42:37 +0530 Subject: [PATCH 12/29] Fix #30: Version update to 2024.5.2 --- eudi-wallet-oidc-android/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eudi-wallet-oidc-android/build.gradle.kts b/eudi-wallet-oidc-android/build.gradle.kts index 9f80c07..6885dd1 100644 --- a/eudi-wallet-oidc-android/build.gradle.kts +++ b/eudi-wallet-oidc-android/build.gradle.kts @@ -72,7 +72,7 @@ publishing { register("release") { groupId = "com.github.decentraliseddataexchange" artifactId = "eudi-wallet-oidc-android" - version = "2024.5.1" + version = "2024.5.2" afterEvaluate { from(components["release"]) From e48fbaec64049eff24192dbe5ead12b0a9d3a6f9 Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 3 Jun 2024 12:10:31 +0530 Subject: [PATCH 13/29] Fix #33 - support for dynamic credential request --- .../services/issue/IssueService.kt | 3 ++- .../services/sdjwt/SDJWTService.kt | 12 ++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index 7a691fb..2016b45 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -216,7 +216,8 @@ class IssueService : IssueServiceInterface { null } - return if (Uri.parse(location).getQueryParameter("code") != null) { + return if (Uri.parse(location).getQueryParameter("code") != null + || Uri.parse(location).getQueryParameter("presentation_definition") != null) { location } else { processAuthorisationRequestUsingIdToken( diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt index fdd844f..c3c5bd8 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/sdjwt/SDJWTService.kt @@ -195,10 +195,14 @@ class SDJWTService : SDJWTServiceInterface { // Filter disclosures based on requested parameters disclosures?.forEach { disclosure -> - val list = - JSONArray(Base64.decode(disclosure, Base64.URL_SAFE).toString(charset("UTF-8"))) - if (list.length() >= 2 && requestedParams.contains(list.optString(1))) { - issuedJwt = "$issuedJwt~$disclosure" + try { + val list = + JSONArray(Base64.decode(disclosure, Base64.URL_SAFE).toString(charset("UTF-8"))) + if (list.length() >= 2 && requestedParams.contains(list.optString(1))) { + issuedJwt = "$issuedJwt~$disclosure" + } + } catch (e: Exception) { + } } From d638d3207c2d1d87e9a998be98e65bbad7ce7147 Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 3 Jun 2024 12:08:39 +0530 Subject: [PATCH 14/29] Fix #32 - Path in presentation submission is updated --- .../services/verification/VerificationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2bdbe1a..525359c 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 @@ -382,7 +382,7 @@ class VerificationService : VerificationServiceInterface { pathNested = PathNested( id = inputDescriptors.id, format = "jwt_vc", - path = "$.verifiableCredential[$index]" + path = "$.vp.verifiableCredential[$index]" ) ) descriptorMap.add(descriptor) From 08fb49892ffd2c507bb48dcbc10b4bf45a7fe760 Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 3 Jun 2024 12:13:38 +0530 Subject: [PATCH 15/29] Fix #36 - Version update to 2024.6.1 --- eudi-wallet-oidc-android/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eudi-wallet-oidc-android/build.gradle.kts b/eudi-wallet-oidc-android/build.gradle.kts index 6885dd1..0d16cd3 100644 --- a/eudi-wallet-oidc-android/build.gradle.kts +++ b/eudi-wallet-oidc-android/build.gradle.kts @@ -72,7 +72,7 @@ publishing { register("release") { groupId = "com.github.decentraliseddataexchange" artifactId = "eudi-wallet-oidc-android" - version = "2024.5.2" + version = "2024.6.1" afterEvaluate { from(components["release"]) From d148ea1f2b02b5ce53a01d8777f53466354fb6bf Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 17 Jun 2024 09:59:59 +0530 Subject: [PATCH 16/29] Fix #38: Handled DiscoveryService to work well when issuer config is not present --- ...thorisationServerWellKnownConfiguration.kt | 4 +++ .../models/IssuerWellKnownConfiguration.kt | 4 +++ .../services/discovery/DiscoveryService.kt | 25 +++++++++++-------- .../discovery/DiscoveryServiceInterface.kt | 6 +++-- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt index c70b506..487c184 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorisationServerWellKnownConfiguration.kt @@ -44,4 +44,8 @@ data class Jwt( @SerializedName("alg") var alg: ArrayList = arrayListOf() +) +data class WrappedAuthConfigResponse( + var authConfig: AuthorisationServerWellKnownConfiguration? = 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/models/IssuerWellKnownConfiguration.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt index 3e66f78..a9f57bf 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/IssuerWellKnownConfiguration.kt @@ -34,4 +34,8 @@ data class Image( @SerializedName("uri") var uri: String? = null, @SerializedName("url") var url: String? = null, @SerializedName("alt_text") var altText: String? = null +) +data class WrappedIssuerConfigResponse( + var issuerConfig: IssuerWellKnownConfiguration? = 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/discovery/DiscoveryService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryService.kt index 5eed5fc..76a19e6 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryService.kt @@ -1,7 +1,10 @@ package com.ewc.eudi_wallet_oidc_android.services.discovery import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfiguration +import com.ewc.eudi_wallet_oidc_android.models.ErrorResponse import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration +import com.ewc.eudi_wallet_oidc_android.models.WrappedAuthConfigResponse +import com.ewc.eudi_wallet_oidc_android.models.WrappedIssuerConfigResponse import com.ewc.eudi_wallet_oidc_android.services.UriValidationFailed import com.ewc.eudi_wallet_oidc_android.services.UrlUtils import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager @@ -12,31 +15,31 @@ class DiscoveryService : DiscoveryServiceInterface { * To fetch the Issue configuration * * @param credentialIssuerWellKnownURI - * @return IssuerWellKnownConfiguration + * @return WrappedIssuerConfigResponse */ - override suspend fun getIssuerConfig(credentialIssuerWellKnownURI: String?): IssuerWellKnownConfiguration? { + override suspend fun getIssuerConfig(credentialIssuerWellKnownURI: String?): WrappedIssuerConfigResponse { try { UrlUtils.validateUri(credentialIssuerWellKnownURI) val response = ApiManager.api.getService() ?.fetchIssuerConfig("$credentialIssuerWellKnownURI") return if (response?.isSuccessful == true) { - response.body() + WrappedIssuerConfigResponse(issuerConfig = response.body(), errorResponse = null) } else { - null + WrappedIssuerConfigResponse(issuerConfig = null, errorResponse = ErrorResponse(error = response?.code(), errorDescription = response?.message())) } } catch (exc: UriValidationFailed) { - return null + return WrappedIssuerConfigResponse(issuerConfig = null, errorResponse = ErrorResponse(error = null, errorDescription = "URI validation failed")) } } /** - * To fetch the authorisation server configuration + * To fetch the authorization server configuration * * @param authorisationServerWellKnownURI - * @return AuthorisationServerWellKnownConfiguration + * @return WrappedAuthConfigResponse */ - override suspend fun getAuthConfig(authorisationServerWellKnownURI: String?): AuthorisationServerWellKnownConfiguration? { + override suspend fun getAuthConfig(authorisationServerWellKnownURI: String?): WrappedAuthConfigResponse { try { UrlUtils.validateUri(authorisationServerWellKnownURI) @@ -44,12 +47,12 @@ class DiscoveryService : DiscoveryServiceInterface { ApiManager.api.getService() ?.fetchAuthConfig("$authorisationServerWellKnownURI") return if (response?.isSuccessful == true) { - response.body() + WrappedAuthConfigResponse(authConfig = response.body(), errorResponse = null) } else { - null + WrappedAuthConfigResponse(authConfig = null, errorResponse = ErrorResponse(error = response?.code(), errorDescription = response?.message())) } } catch (exc: UriValidationFailed) { - return null + return WrappedAuthConfigResponse(authConfig = null, errorResponse = ErrorResponse(error = null, errorDescription = "URI validation failed")) } } } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt index bc2220f..cddb4b3 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/discovery/DiscoveryServiceInterface.kt @@ -2,9 +2,11 @@ package com.ewc.eudi_wallet_oidc_android.services.discovery import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration +import com.ewc.eudi_wallet_oidc_android.models.WrappedAuthConfigResponse +import com.ewc.eudi_wallet_oidc_android.models.WrappedIssuerConfigResponse interface DiscoveryServiceInterface { - suspend fun getIssuerConfig(credentialIssuerWellKnownURI:String?):IssuerWellKnownConfiguration? + suspend fun getIssuerConfig(credentialIssuerWellKnownURI:String?):WrappedIssuerConfigResponse? - suspend fun getAuthConfig(authorisationServerWellKnownURI:String?):AuthorisationServerWellKnownConfiguration? + suspend fun getAuthConfig(authorisationServerWellKnownURI:String?): WrappedAuthConfigResponse? } \ No newline at end of file From 59fb726f9d783d09ebe50ecab43c5a0166ad89f8 Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 17 Jun 2024 10:02:21 +0530 Subject: [PATCH 17/29] Fix #39: Support EWC RFC for CredentialRequest --- .../models/CredentialRequest.kt | 1 + .../services/issue/IssueService.kt | 148 ++++++++++++------ .../services/issue/IssueServiceInterface.kt | 2 +- 3 files changed, 104 insertions(+), 47 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt index 4da2222..e270728 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/CredentialRequest.kt @@ -7,6 +7,7 @@ data class CredentialRequest( @SerializedName("types") var types: ArrayList? = null, @SerializedName("credential_definition") var credentialDefinition: CredentialDefinition? = null, + @SerializedName("vct") var vct: String? = null, @SerializedName("format") var format: String? = null, @SerializedName("proof") var proof: ProofV3? = null diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index 2016b45..fe63aa3 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -16,6 +16,8 @@ import com.ewc.eudi_wallet_oidc_android.models.ProofV3 import com.ewc.eudi_wallet_oidc_android.models.VpFormatsSupported import com.ewc.eudi_wallet_oidc_android.models.WrappedCredentialResponse import com.ewc.eudi_wallet_oidc_android.models.WrappedTokenResponse +import com.ewc.eudi_wallet_oidc_android.services.UriValidationFailed +import com.ewc.eudi_wallet_oidc_android.services.UrlUtils import com.ewc.eudi_wallet_oidc_android.services.codeVerifier.CodeVerifierService import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager import com.google.gson.Gson @@ -46,23 +48,28 @@ class IssueService : IssueServiceInterface { */ override suspend fun resolveCredentialOffer(data: String?): CredentialOffer? { if (data.isNullOrBlank()) return null - - val uri = Uri.parse(data) - val credentialOfferUri = uri.getQueryParameter("credential_offer_uri") - if (!credentialOfferUri.isNullOrBlank()) { - val response = ApiManager.api.getService()?.resolveCredentialOffer(credentialOfferUri) - return if (response?.isSuccessful == true) { - response.body() - } else { - null + try { + val uri = Uri.parse(data) + val credentialOfferUri = uri.getQueryParameter("credential_offer_uri") + UrlUtils.validateUri(credentialOfferUri) + if (!credentialOfferUri.isNullOrBlank()) { + val response = + ApiManager.api.getService()?.resolveCredentialOffer(credentialOfferUri) + return if (response?.isSuccessful == true) { + response.body() + } else { + null + } } - } - val credentialOfferString = uri.getQueryParameter("credential_offer") - if (!credentialOfferString.isNullOrBlank()) { - return Gson().fromJson(credentialOfferString, CredentialOffer::class.java) + val credentialOfferString = uri.getQueryParameter("credential_offer") + if (!credentialOfferString.isNullOrBlank()) { + return Gson().fromJson(credentialOfferString, CredentialOffer::class.java) + } + return null + } catch (exc: UriValidationFailed) { + return null } - return null } @@ -217,7 +224,8 @@ class IssueService : IssueServiceInterface { } return if (Uri.parse(location).getQueryParameter("code") != null - || Uri.parse(location).getQueryParameter("presentation_definition") != null) { + || Uri.parse(location).getQueryParameter("presentation_definition") != null + ) { location } else { processAuthorisationRequestUsingIdToken( @@ -246,17 +254,22 @@ class IssueService : IssueServiceInterface { .build() // Create JWT for ES256K alg - val jwsHeader = JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) - .type(JOSEObjectType.JWT) - .keyID("$did#${did?.replace("did:key:", "")}") - .build() + val jwsHeader = + JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType.JWT) + .keyID("$did#${did?.replace("did:key:", "")}") + .build() val jwt = SignedJWT( jwsHeader, claimsSet ) // Sign with private EC key - jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner(subJwk as ECKey)) + jwt.sign( + if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner( + subJwk as ECKey + ) + ) val response = ApiManager.api.getService()?.sendIdTokenForCode( url = Uri.parse(location).getQueryParameter("redirect_uri") ?: "", @@ -359,7 +372,7 @@ class IssueService : IssueServiceInterface { credentialOffer: CredentialOffer?, credentialIssuerEndPoint: String?, accessToken: String?, - format:String + format: String ): WrappedCredentialResponse? { // Add claims @@ -477,7 +490,11 @@ class IssueService : IssueServiceInterface { val jwt = SignedJWT( jwsHeader, claimsSet ) - jwt.sign(if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner(subJwk as ECKey)) + jwt.sign( + if (subJwk is OctetKeyPair) Ed25519Signer(subJwk as OctetKeyPair) else ECDSASigner( + subJwk as ECKey + ) + ) // Construct credential request val body = buildCredentialRequest( @@ -495,12 +512,6 @@ class IssueService : IssueServiceInterface { ) val credentialResponse = when { - response?.isSuccessful == true -> { - WrappedCredentialResponse( - credentialResponse = response.body() - ) - } - (response?.code() ?: 0) >= 400 -> { try { WrappedCredentialResponse( @@ -511,6 +522,12 @@ class IssueService : IssueServiceInterface { } } + response?.isSuccessful == true -> { + WrappedCredentialResponse( + credentialResponse = response.body() + ) + } + else -> { null } @@ -541,10 +558,31 @@ class IssueService : IssueServiceInterface { if (credentialDefinitionNeeded) { var types: ArrayList? = getTypesFromCredentialOffer(credentialOffer) - try { - types = getTypesFromIssuerConfig(issuerConfig, type = types?.last()) - } catch (e: Exception) { + when (val data = getTypesFromIssuerConfig( + issuerConfig, + type = if (types?.isNotEmpty() == true) types.last() else "" + )) { + is ArrayList<*> -> { + return CredentialRequest( + credentialDefinition = CredentialDefinition(type = data as ArrayList), + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } + is String -> { + return CredentialRequest( + vct = data as String, + format = format, + proof = ProofV3( + proofType = "jwt", + jwt = jwt + ) + ) + } } return CredentialRequest( @@ -707,7 +745,7 @@ class IssueService : IssueServiceInterface { override fun getTypesFromIssuerConfig( issuerConfig: IssuerWellKnownConfiguration?, type: String? - ): ArrayList? { + ): Any? { var types: ArrayList = ArrayList() val credentialOfferJsonString = Gson().toJson(issuerConfig) val jsonObject = JSONObject(credentialOfferJsonString) @@ -717,13 +755,22 @@ class IssueService : IssueServiceInterface { is JSONObject -> { try { val credentialSupported = credentialsSupported.getJSONObject(type ?: "") - val typeFromCredentialIssuer = - credentialSupported.getJSONObject("credential_definition") - .getJSONArray("type") - for (i in 0 until typeFromCredentialIssuer.length()) { - // Get each JSONObject from the JSONArray - val type: String = typeFromCredentialIssuer.getString(i) - types.add(type) + val format = + if (credentialSupported.has("format")) credentialSupported.getString("format") else "" + + if (format == "vc+sd-jwt") { + return credentialSupported.getJSONObject("credential_definition") + .getString("vct") + } else { + val typeFromCredentialIssuer: JSONArray = + credentialSupported.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + return types } } catch (e: Exception) { } @@ -740,13 +787,22 @@ class IssueService : IssueServiceInterface { // Check if the string is present in the "types" array for (j in 0 until typesArray.length()) { if (typesArray.getString(j) == type) { - val typeFromCredentialIssuer = - jsonObject.getJSONObject("credential_definition") - .getJSONArray("type") - for (i in 0 until typeFromCredentialIssuer.length()) { - // Get each JSONObject from the JSONArray - val type: String = typeFromCredentialIssuer.getString(i) - types.add(type) + val format = + if (jsonObject.has("format")) jsonObject.getString("format") else "" + + if (format == "vc+sd-jwt") { + return jsonObject.getJSONObject("credential_definition") + .getString("vct") + } else { + val typeFromCredentialIssuer: JSONArray = + jsonObject.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + return types } break } diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt index dbf1868..70b0355 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueServiceInterface.kt @@ -179,7 +179,7 @@ interface IssueServiceInterface { fun getTypesFromIssuerConfig( issuerConfig: IssuerWellKnownConfiguration?, type: String? - ): ArrayList? + ): Any? /** * Get types from Issuer Config From 040cb12a925165277a15ddb12f497cbe57ae4868 Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 17 Jun 2024 10:04:18 +0530 Subject: [PATCH 18/29] Fix #40: Added default format for presentation submission --- .../services/verification/VerificationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 525359c..088c753 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 @@ -378,7 +378,7 @@ class VerificationService : VerificationServiceInterface { val descriptor = DescriptorMap( id = inputDescriptors.id, path = "$", - format = presentationDefinition.format?.keys?.first(), + format = presentationDefinition.format?.keys?.firstOrNull()?:"jwt_vp", pathNested = PathNested( id = inputDescriptors.id, format = "jwt_vc", From 2a03e023c23a9e11fbe1f8df1df26ec858128001 Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 17 Jun 2024 10:04:42 +0530 Subject: [PATCH 19/29] Fix : Updated sample project --- .../ewc/eudiwalletoidcandroid/MainActivity.kt | 2 +- .../eudiwalletoidcandroid/MainViewModel.kt | 61 +++++++++---------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt index 852dfc2..3045ad9 100644 --- a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt +++ b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainActivity.kt @@ -150,7 +150,7 @@ class MainActivity : AppCompatActivity() { viewModel?.displayText?.value = "${viewModel?.displayText?.value}Scanned data : $url\n\n" - viewModel?.issueCredential(url ?: "") + viewModel?.issueCredential(url ?: "",this) } REQUEST_CODE_SCAN_VERIFY -> { diff --git a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt index b16935b..6507908 100644 --- a/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt +++ b/app/src/main/java/com/ewc/eudiwalletoidcandroid/MainViewModel.kt @@ -1,6 +1,8 @@ package com.ewc.eudiwalletoidcandroid +import android.content.Context import android.net.Uri +import android.widget.Toast import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.ewc.eudi_wallet_oidc_android.CryptographicAlgorithms @@ -51,48 +53,37 @@ class MainViewModel : ViewModel() { isLoading.value = false } - fun issueCredential(url: String) { + fun issueCredential(url: String,context:Context) { isLoading.value = true CoroutineScope(Dispatchers.Main).launch { // Resolving credential offer offerCredential = IssueService().resolveCredentialOffer(url) - // Discovery - issuerConfig = - DiscoveryService().getIssuerConfig("${offerCredential?.credentialIssuer}/.well-known/openid-credential-issuer") - - authConfig = - DiscoveryService().getAuthConfig( - "${issuerConfig?.authorizationServer - ?: issuerConfig?.authorizationServers?.get(0 - )?: issuerConfig?.issuer}/.well-known/openid-configuration" - ) - types = IssueService().getTypesFromCredentialOffer(offerCredential) - format = - IssueService().getFormatFromIssuerConfig(issuerConfig, types.lastOrNull() ?: "") - - val cryptoSupported = IssueService().getCryptoFromIssuerConfig( - issuerConfig, - types.lastOrNull() ?: "ES256" + val wrappedResponse = DiscoveryService().getIssuerConfig("${offerCredential?.credentialIssuer}/.well-known/openid-credential-issuer") + if (wrappedResponse.issuerConfig != null) { + // Handle successful response + issuerConfig = wrappedResponse.issuerConfig + } else { + displayErrorMessage(context, wrappedResponse.errorResponse?.errorDescription) + return@launch + } + val wrappedAuthResponse = DiscoveryService().getAuthConfig( + "${issuerConfig?.authorizationServer ?: issuerConfig?.issuer}/.well-known/openid-configuration" ) + if(wrappedAuthResponse.authConfig !=null){ + // Handle successful response + authConfig = wrappedAuthResponse.authConfig + } + else{ + displayErrorMessage(context, wrappedAuthResponse.errorResponse?.errorDescription) + return@launch + } - subJwk = DIDService().createJWK( - null, - cryptoSupported?.lastOrNull() ?: CryptographicAlgorithms.ES256 - ) - did = DIDService().createDID( - subJwk, - cryptoSupported?.lastOrNull() ?: CryptographicAlgorithms.ES256 - ) - // Generating code verifier codeVerifier = CodeVerifierService().generateCodeVerifier() withContext(Dispatchers.Main) { - displayText.value = "Sub JWK : \n ${Gson().toJson(subJwk)}\n\n" - displayText.value = - "${displayText.value}Did : $did\n\n" displayText.value = - "${displayText.value}Issuer Config : \n${Gson().toJson(issuerConfig)}\n\n" + "${displayText?.value}Issuer Config : \n${Gson().toJson(issuerConfig)}\n\n" displayText.value = "${displayText.value}Auth Config : \n${Gson().toJson(authConfig)}\n\n" displayText.value = @@ -121,7 +112,7 @@ class MainViewModel : ViewModel() { // Process Authorisation request val authResponse = IssueService().processAuthorisationRequest( did, - subJwk as ECKey, + subJwk, offerCredential, codeVerifier, authConfig?.authorizationEndpoint @@ -144,6 +135,12 @@ class MainViewModel : ViewModel() { } } + private fun displayErrorMessage(context: Context, errorMessage: String?) { + val messageToShow = errorMessage?.takeIf { it.isNotBlank() } ?: "Unknown error" + Toast.makeText(context, messageToShow, Toast.LENGTH_SHORT).show() + isLoading.value = false + } + private suspend fun getCredential() { val credential = IssueService().processCredentialRequest( From 5612dbeab9e9202695f2120bcc14a9338957f23e Mon Sep 17 00:00:00 2001 From: milan Date: Fri, 28 Jun 2024 16:11:25 +0530 Subject: [PATCH 20/29] Fix : Updated input descriptors --- .../ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt index f0c10cd..547840b 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/InputDescriptors.kt @@ -34,6 +34,7 @@ data class Filter( data class Contains( - @SerializedName("const") var const: String? = null - + @SerializedName("const") var const: String? = null, + @SerializedName("pattern") var pattern: String? = null, + @SerializedName("type") var type: String? = null ) \ No newline at end of file From 6872667b543545cfacbc079977b387418cefc92e Mon Sep 17 00:00:00 2001 From: milan Date: Fri, 28 Jun 2024 16:45:37 +0530 Subject: [PATCH 21/29] Fix : Authorization request to support EWC RFC --- .../models/AuthorizationDetails.kt | 8 ++- .../services/issue/IssueService.kt | 59 +++++++++++++------ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt index 4c039e2..9e35957 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/AuthorizationDetails.kt @@ -1,12 +1,16 @@ package com.ewc.eudi_wallet_oidc_android.models import com.google.gson.annotations.SerializedName +data class CredentialTypeDefinition( + @SerializedName("type") var type: ArrayList? = arrayListOf() +) data class AuthorizationDetails( @SerializedName("type") var type: String? = "openid_credential", - @SerializedName("format") var format: String? = "jwt_vc", + @SerializedName("format") var format: String? = null, @SerializedName("types") var types: ArrayList? = arrayListOf(), - @SerializedName("locations") var locations: ArrayList? = arrayListOf() + @SerializedName("locations") var locations: ArrayList? = arrayListOf(), + @SerializedName("credential_definition") var credentialDefinition: CredentialTypeDefinition? = CredentialTypeDefinition() ) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index fe63aa3..d2fe320 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -9,6 +9,7 @@ import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV1 import com.ewc.eudi_wallet_oidc_android.models.CredentialOfferV2 import com.ewc.eudi_wallet_oidc_android.models.CredentialRequest +import com.ewc.eudi_wallet_oidc_android.models.CredentialTypeDefinition import com.ewc.eudi_wallet_oidc_android.models.ErrorResponse import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.Jwt @@ -95,15 +96,7 @@ class IssueService : IssueServiceInterface { val scope = "openid" val state = UUID.randomUUID().toString() val clientId = did - val authorisationDetails = Gson().toJson( - arrayListOf( - AuthorizationDetails( - types = getTypesFromCredentialOffer(credentialOffer), - locations = arrayListOf(credentialOffer?.credentialIssuer ?: "") - ) - ) - ) - + val authorisationDetails = buildAuthorizationRequest(credentialOffer) val redirectUri = "http://localhost:8080" val nonce = UUID.randomUUID().toString() @@ -176,14 +169,7 @@ class IssueService : IssueServiceInterface { val scope = "openid" val state = UUID.randomUUID().toString() val clientId = did - val authorisationDetails = Gson().toJson( - arrayListOf( - AuthorizationDetails( - types = getTypesFromCredentialOffer(credentialOffer), - locations = arrayListOf(credentialOffer?.credentialIssuer ?: "") - ) - ) - ) + val authorisationDetails = buildAuthorizationRequest(credentialOffer) val redirectUri = "http://localhost:8080" val nonce = UUID.randomUUID().toString() @@ -285,6 +271,45 @@ class IssueService : IssueServiceInterface { } } + private fun buildAuthorizationRequest(credentialOffer: CredentialOffer?):String{ + val gson = Gson() + var credentialDefinitionNeeded = false + try { + val credentialOfferV1 = + gson.fromJson(gson.toJson(credentialOffer), CredentialOfferV1::class.java) + + if (credentialOfferV1?.credentials?.get(0)?.trustFramework == null) + credentialDefinitionNeeded = true + + } catch (e: Exception) { + credentialDefinitionNeeded = true + } + if (credentialDefinitionNeeded) { + return gson.toJson( + arrayListOf( + AuthorizationDetails( + format = "jwt_vc_json", + locations = arrayListOf(credentialOffer?.credentialIssuer ?: ""), + credentialDefinition = CredentialTypeDefinition( + type = getTypesFromCredentialOffer(credentialOffer) + ) + ) + ) + ) + + }else{ + return gson.toJson( + arrayListOf( + AuthorizationDetails( + format = "jwt_vc", + types = getTypesFromCredentialOffer(credentialOffer), + locations = arrayListOf(credentialOffer?.credentialIssuer ?: "") + ) + ) + ) + } + } + /** * To process the token, * From f17c5fd67711957b02b9b2ec09ec751af7f84789 Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 16 Jul 2024 12:42:09 +0530 Subject: [PATCH 22/29] Fix : process error during send VP Token --- .../models/WrappedVpTokenResponse.kt | 10 ++ .../services/issue/IssueService.kt | 145 +++++++++++------- .../verification/VerificationService.kt | 44 ++++-- .../VerificationServiceInterface.kt | 3 +- 4 files changed, 131 insertions(+), 71 deletions(-) create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/WrappedVpTokenResponse.kt diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/WrappedVpTokenResponse.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/WrappedVpTokenResponse.kt new file mode 100644 index 0000000..3e2ca67 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/WrappedVpTokenResponse.kt @@ -0,0 +1,10 @@ +package com.ewc.eudi_wallet_oidc_android.models +import com.google.gson.annotations.SerializedName +data class VPTokenResponse( + @SerializedName("location") var location: String? = null, +) + +data class WrappedVpTokenResponse( + var vpTokenResponse: VPTokenResponse? = 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/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index d2fe320..41e307c 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -33,6 +33,7 @@ import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT import org.json.JSONArray +import org.json.JSONException import org.json.JSONObject import java.util.Date import java.util.UUID @@ -204,12 +205,18 @@ class IssueService : IssueServiceInterface { ) val location: String? = if (response?.code() == 302) { - response.headers()["Location"] + if (response.headers()["Location"]?.contains("error") == true || response.headers()["Location"]?.contains("error_description") == true) { + response.headers()["Location"] + } else { + response.headers()["Location"] + } } else { null } - return if (Uri.parse(location).getQueryParameter("code") != null + return if(Uri.parse(location).getQueryParameter("error") != null) { + location + }else if (Uri.parse(location).getQueryParameter("code") != null || Uri.parse(location).getQueryParameter("presentation_definition") != null ) { location @@ -636,6 +643,7 @@ class IssueService : IssueServiceInterface { // 2. {"error_description": "Validation is failed", } // 3. {"errors": [{ "message": "Validation is failed" }]} // 4. {"error": "Validation is failed"} + // 5. {"detail": "VC token expired"} val jsonObject = try { err?.let { JSONObject(it) } } catch (e: Exception) { @@ -671,6 +679,13 @@ class IssueService : IssueServiceInterface { ) } + jsonObject?.has("detail") == true -> { + ErrorResponse( + error = -1, + errorDescription = jsonObject.getString("detail") + ) + } + else -> { null } @@ -772,75 +787,87 @@ class IssueService : IssueServiceInterface { type: String? ): Any? { var types: ArrayList = ArrayList() - val credentialOfferJsonString = Gson().toJson(issuerConfig) - val jsonObject = JSONObject(credentialOfferJsonString) - - val credentialsSupported: Any = jsonObject.opt("credentials_supported") ?: return null - when (credentialsSupported) { - is JSONObject -> { - try { - val credentialSupported = credentialsSupported.getJSONObject(type ?: "") - val format = - if (credentialSupported.has("format")) credentialSupported.getString("format") else "" - - if (format == "vc+sd-jwt") { - return credentialSupported.getJSONObject("credential_definition") - .getString("vct") - } else { - val typeFromCredentialIssuer: JSONArray = - credentialSupported.getJSONObject("credential_definition") - .getJSONArray("type") - for (i in 0 until typeFromCredentialIssuer.length()) { - // Get each JSONObject from the JSONArray - val type: String = typeFromCredentialIssuer.getString(i) - types.add(type) + // Check if issuerConfig is null + if (issuerConfig == null) { + return null + } + try { + val credentialOfferJsonString = Gson().toJson(issuerConfig) + // Check if credentialOfferJsonString is null or empty + if (credentialOfferJsonString.isNullOrEmpty()) { + return null + } + val jsonObject = JSONObject(credentialOfferJsonString) + + val credentialsSupported: Any = jsonObject.opt("credentials_supported") ?: return null + when (credentialsSupported) { + is JSONObject -> { + try { + val credentialSupported = credentialsSupported.getJSONObject(type ?: "") + val format = + if (credentialSupported.has("format")) credentialSupported.getString("format") else "" + + if (format == "vc+sd-jwt") { + return credentialSupported.getJSONObject("credential_definition") + .getString("vct") + } else { + val typeFromCredentialIssuer: JSONArray = + credentialSupported.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + return types } - return types + } catch (e: Exception) { } - } catch (e: Exception) { } - } - - is JSONArray -> { - try { - for (i in 0 until credentialsSupported.length()) { - val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) - - // Get the "types" JSONArray - val typesArray = jsonObject.getJSONArray("types") - // Check if the string is present in the "types" array - for (j in 0 until typesArray.length()) { - if (typesArray.getString(j) == type) { - val format = - if (jsonObject.has("format")) jsonObject.getString("format") else "" - - if (format == "vc+sd-jwt") { - return jsonObject.getJSONObject("credential_definition") - .getString("vct") - } else { - val typeFromCredentialIssuer: JSONArray = - jsonObject.getJSONObject("credential_definition") - .getJSONArray("type") - for (i in 0 until typeFromCredentialIssuer.length()) { - // Get each JSONObject from the JSONArray - val type: String = typeFromCredentialIssuer.getString(i) - types.add(type) + is JSONArray -> { + try { + for (i in 0 until credentialsSupported.length()) { + val jsonObject: JSONObject = credentialsSupported.getJSONObject(i) + + // Get the "types" JSONArray + val typesArray = jsonObject.getJSONArray("types") + + // Check if the string is present in the "types" array + for (j in 0 until typesArray.length()) { + if (typesArray.getString(j) == type) { + val format = + if (jsonObject.has("format")) jsonObject.getString("format") else "" + + if (format == "vc+sd-jwt") { + return jsonObject.getJSONObject("credential_definition") + .getString("vct") + } else { + val typeFromCredentialIssuer: JSONArray = + jsonObject.getJSONObject("credential_definition") + .getJSONArray("type") + for (i in 0 until typeFromCredentialIssuer.length()) { + // Get each JSONObject from the JSONArray + val type: String = typeFromCredentialIssuer.getString(i) + types.add(type) + } + return types } - return types + break } - break } } + } catch (e: Exception) { } - } catch (e: Exception) { } - } - else -> { - // Neither JSONObject nor JSONArray - println("Child is neither JSONObject nor JSONArray") + else -> { + // Neither JSONObject nor JSONArray + println("Child is neither JSONObject nor JSONArray") + } } + }catch (e: JSONException){ + Log.e("getTypesFromIssuerConfig", "Error parsing JSON", e) } return types 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 088c753..9f2ecbb 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 @@ -3,10 +3,14 @@ package com.ewc.eudi_wallet_oidc_android.services.verification import android.net.Uri import android.util.Base64 import com.ewc.eudi_wallet_oidc_android.models.DescriptorMap +import com.ewc.eudi_wallet_oidc_android.models.ErrorResponse import com.ewc.eudi_wallet_oidc_android.models.PathNested 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.PresentationSubmission +import com.ewc.eudi_wallet_oidc_android.models.VPTokenResponse +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 import com.ewc.eudi_wallet_oidc_android.services.sdjwt.SDJWTService import com.github.decentraliseddataexchange.presentationexchangesdk.PresentationExchange @@ -26,6 +30,7 @@ import com.nimbusds.jwt.JWT import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.JWTParser import com.nimbusds.jwt.SignedJWT +import org.json.JSONArray import org.json.JSONObject import java.util.Date import java.util.UUID @@ -207,7 +212,7 @@ class VerificationService : VerificationServiceInterface { subJwk: JWK?, presentationRequest: PresentationRequest, credentialList: List - ): String? { + ): WrappedVpTokenResponse? { val iat = Date() val jti = "urn:uuid:${UUID.randomUUID()}" val claimsSet = JWTClaimsSet.Builder() @@ -232,11 +237,12 @@ class VerificationService : VerificationServiceInterface { ).build() // Create JWT for ES256K alg - val jwsHeader = JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) - .type(JOSEObjectType("JWT")) - .keyID("$did#${did?.replace("did:key:", "")}") - .jwk(subJwk?.toPublicJWK()) - .build() + val jwsHeader = + JWSHeader.Builder(if (subJwk is OctetKeyPair) JWSAlgorithm.EdDSA else JWSAlgorithm.ES256) + .type(JOSEObjectType("JWT")) + .keyID("$did#${did?.replace("did:key:", "")}") + .jwk(subJwk?.toPublicJWK()) + .build() val jwt = SignedJWT( jwsHeader, @@ -259,11 +265,27 @@ class VerificationService : VerificationServiceInterface { ) ) - return if (response?.code() == 302 || response?.code() == 200) { - response.headers()["Location"] ?: "https://tid-wallet-poc.azurewebsites.net?code=1" - } else { - null + val tokenResponse = when { + response?.code() == 302 || response?.code() == 200 -> { + WrappedVpTokenResponse( + vpTokenResponse = VPTokenResponse( + location = response.headers()["Location"] + ?: "https://www.example.com?code=1" + ) + ) + } + + (response?.code() ?: 0) >= 400 -> { + WrappedVpTokenResponse( + errorResponse = IssueService().processError(response?.errorBody()?.string()) + ) + } + + else -> { + null + } } + return tokenResponse } /** @@ -378,7 +400,7 @@ class VerificationService : VerificationServiceInterface { val descriptor = DescriptorMap( id = inputDescriptors.id, path = "$", - format = presentationDefinition.format?.keys?.firstOrNull()?:"jwt_vp", + format = presentationDefinition.format?.keys?.firstOrNull() ?: "jwt_vp", pathNested = PathNested( id = inputDescriptors.id, format = "jwt_vc", 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 44061ce..419eddf 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.WrappedVpTokenResponse import com.github.decentraliseddataexchange.presentationexchangesdk.PresentationExchange import com.github.decentraliseddataexchange.presentationexchangesdk.models.MatchedCredential import com.google.gson.Gson @@ -54,7 +55,7 @@ interface VerificationServiceInterface { subJwk: JWK?, presentationRequest: PresentationRequest, credentialList: List - ): String? + ): WrappedVpTokenResponse? /** * To filter the credential using the input descriptors From e9f761472fa5c24a5ed023b3f3654f2c5e05244a Mon Sep 17 00:00:00 2001 From: milan Date: Tue, 16 Jul 2024 12:44:15 +0530 Subject: [PATCH 23/29] Fix #41: Updated filtering function to support combination of JWT and SDJWT --- .../verification/VerificationService.kt | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) 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 9f2ecbb..c638d37 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 @@ -295,24 +295,43 @@ class VerificationService : VerificationServiceInterface { allCredentialList: List, presentationDefinition: PresentationDefinition ): List> { - //list of credentials matched for all input descriptors + val response: MutableList> = mutableListOf() + val pex = PresentationExchange() - val credentialList: ArrayList = arrayListOf() - for (item in allCredentialList) { - if (presentationDefinition.inputDescriptors?.get(0)?.constraints?.limitDisclosure != null && item?.contains( - "~" - ) == true - ) - credentialList.add(item) - else if (presentationDefinition.inputDescriptors?.get(0)?.constraints?.limitDisclosure == null && item?.contains( - "~" - ) != true - ) - credentialList.add(item) + presentationDefinition.inputDescriptors?.forEach { inputDescriptors -> + val credentialList = splitCredentialsBySdJWT(allCredentialList, inputDescriptors.constraints?.limitDisclosure != null) + val processedCredentials = processCredentialsToJsonString(credentialList) + val filteredCredentialList: MutableList = mutableListOf() + val inputDescriptor = Gson().toJson(inputDescriptors) + + val matches: List = + pex.matchCredentials(inputDescriptor, processedCredentials) + + for (match in matches) { + filteredCredentialList.add(credentialList[match.index] ?: "") + } + + response.add(filteredCredentialList) } - val response: MutableList> = mutableListOf() + return response + } + + private fun splitCredentialsBySdJWT( + allCredentials: List, + isSdJwt: Boolean + ): ArrayList { + val filteredCredentials: ArrayList = arrayListOf() + for (item in allCredentials) { + if (isSdJwt && item?.contains("~") == true) + filteredCredentials.add(item) + else if (!isSdJwt && item?.contains("~") != null) + filteredCredentials.add(item) + } + return filteredCredentials + } + private fun processCredentialsToJsonString(credentialList: ArrayList):List{ var processedCredentials: List = mutableListOf() for (cred in credentialList) { val split = cred?.split(".") @@ -335,24 +354,7 @@ class VerificationService : VerificationServiceInterface { else json.toString() ) } - - val pex = PresentationExchange() - - presentationDefinition.inputDescriptors?.forEach { inputDescriptors -> - val filteredCredentialList: MutableList = mutableListOf() - val inputDescriptor = Gson().toJson(inputDescriptors) - - val matches: List = - pex.matchCredentials(inputDescriptor, processedCredentials) - - for (match in matches) { - filteredCredentialList.add(credentialList[match.index] ?: "") - } - - response.add(filteredCredentialList) - } - - return response + return processedCredentials } /** From 5827aac93188068cd1f1dc84b22633e9fff972bf Mon Sep 17 00:00:00 2001 From: milan Date: Wed, 17 Jul 2024 21:32:06 +0530 Subject: [PATCH 24/29] Fix #42 #43: Credential validation for expiry and signature --- .../eudi_wallet_oidc_android/models/JwkKey.kt | 11 ++ .../models/JwksResponse.kt | 4 + .../CredentialValidator.kt | 36 ++++ .../CredentialValidatorInterface.kt | 15 ++ .../credentialValidation/ExpiryValidator.kt | 28 +++ .../SignatureValidator.kt | 175 ++++++++++++++++++ .../services/did/DIDService.kt | 36 ++++ .../services/did/DIDServiceInterface.kt | 8 + .../services/exceptions/ExpiryException.kt | 3 + .../services/exceptions/SignatureException.kt | 3 + 10 files changed, 319 insertions(+) create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt new file mode 100644 index 0000000..5db5238 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwkKey.kt @@ -0,0 +1,11 @@ +package com.ewc.eudi_wallet_oidc_android.models + +// Data class representing a JSON Web Key (JWK). +data class JwkKey( + val kty: String, + val kid: String, + val crv: String, + val x: String, + val y: String, + val use: String +) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt new file mode 100644 index 0000000..18d061f --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/JwksResponse.kt @@ -0,0 +1,4 @@ +package com.ewc.eudi_wallet_oidc_android.models + +// Data class representing a response containing a list of JSON Web Keys (JWKs). +data class JwksResponse(val keys: List) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt new file mode 100644 index 0000000..52398a7 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidator.kt @@ -0,0 +1,36 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation + +import com.ewc.eudi_wallet_oidc_android.services.exceptions.ExpiryException +import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException + +class CredentialValidator:CredentialValidatorInterface { + + /** + * Validates a JWT credential by checking its expiration and signature. + * + * @param jwt + * @param jwksUri + * @return + * + * Returns true if the JWT is valid; otherwise, throws IllegalArgumentException with appropriate messages. + */ + @Throws(IllegalArgumentException::class) + override suspend fun validateCredential(jwt: String?, jwksUri: String?): Boolean { + try { + // Check if the JWT has expired + ExpiryValidator().isJwtExpired(jwt = jwt) + + // Validate the JWT signature using the provided JWKS URI + SignatureValidator().validateSignature(jwt = jwt, jwksUri = jwksUri) + + // If both checks pass, return true indicating the credential is valid + return true + } catch (expiryException: ExpiryException) { + // Throw IllegalArgumentException if JWT is expired + throw IllegalArgumentException("JWT token expired") + } catch (signatureException: SignatureException) { + // Throw IllegalArgumentException if JWT signature is invalid + throw IllegalArgumentException("JWT signature invalid") + } + } +} diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt new file mode 100644 index 0000000..e3965ec --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/CredentialValidatorInterface.kt @@ -0,0 +1,15 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation + +interface CredentialValidatorInterface { + /** + * Validates a JWT credential by checking its expiration and signature. + * + * @param jwt + * @param jwksUri + * @return + * + * Returns true if the JWT is valid; otherwise, throws IllegalArgumentException with appropriate messages. + */ + @Throws(IllegalArgumentException::class) + suspend fun validateCredential(jwt: String?,jwksUri:String?):Boolean +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt new file mode 100644 index 0000000..4fee784 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt @@ -0,0 +1,28 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation + +import com.ewc.eudi_wallet_oidc_android.services.exceptions.ExpiryException +import com.nimbusds.jwt.SignedJWT +import java.text.ParseException +import java.util.Date + +class ExpiryValidator { + /** + * Checks if the provided JWT (JSON Web Token) has expired. + * + * @param jwt + * @return + * + * Returns true if the JWT is expired, false otherwise. + * Throws ExpiryException if parsing the JWT or checking expiration encounters errors. + */ + @Throws(ExpiryException::class) + fun isJwtExpired(jwt: String?): Boolean { + return try { + val signedJWT = SignedJWT.parse(jwt) + val expirationTime = signedJWT.jwtClaimsSet.expirationTime + expirationTime?.before(Date()) ?: throw ExpiryException("JWT token expired") + } catch (e: ParseException) { + throw ExpiryException("JWT token expired", e) + } + } +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt new file mode 100644 index 0000000..380b0e3 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt @@ -0,0 +1,175 @@ +package com.ewc.eudi_wallet_oidc_android.services.credentialValidation + +import com.ewc.eudi_wallet_oidc_android.models.JwkKey +import com.ewc.eudi_wallet_oidc_android.models.JwksResponse +import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException +import com.ewc.eudi_wallet_oidc_android.services.did.DIDService +import com.google.gson.Gson +import com.nimbusds.jose.JWSObject +import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.jwk.Curve +import com.nimbusds.jose.jwk.ECKey +import com.nimbusds.jose.jwk.JWK +import com.nimbusds.jose.util.Base64URL +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.URL + +class SignatureValidator { + + /** + * Validates the signature of a JWT using a JWK fetched either from + * Kid or JWKS URI present in Authorisation configuration. + * + * @param jwt + * @param jwksUri + * @return + * + * Throws SignatureException if validation fails + */ + @Throws(SignatureException::class) + suspend fun validateSignature(jwt: String?,jwksUri:String?=null): Boolean { + return try { + jwt?.let { + val jwsObject = JWSObject.parse(jwt) + val header = jwsObject.header + val kid = header.keyID + // Check the format of kid and process accordingly + val response = if (kid.startsWith("did:key:z")) { + processJWKFromKID(kid) + } else { + processJWKFromJwksUri(kid,jwksUri) + } + if (response != null) { + val isSignatureValid = verifyJwtSignature(jwt, response.toJSONString()) + isSignatureValid + } else { + throw SignatureException("Invalid signature") + } + } ?: throw SignatureException("Invalid signature") + } catch (e: IllegalArgumentException) { + throw SignatureException("Invalid signature") + } + } + + + /** + * Processes a JWK from a DID + * + * @param did + * @return + */ + private fun processJWKFromKID(did: String?): JWK? { + try { + if (did == null || !did.startsWith("did:key:z")) { + throw IllegalArgumentException("Invalid DID format") + } + // Extract the multiBaseEncoded part + val multiBaseEncoded = if (did.contains("#")) { + did.split("#")[0].substring("did:key:z".length) + } else { + did.substring("did:key:z".length) + } + // Call convertDIDToJWK function from DIDService + return DIDService().convertDIDToJWK(multiBaseEncoded) + } catch (e: IllegalArgumentException) { + // Handle specific exception if needed + throw IllegalArgumentException("Error converting DID to JWK", e) + } catch (e: Exception) { + // Handle other exceptions + throw IllegalArgumentException("Error converting DID to JWK", e) + } + + } + + + /** + * Verifies the signature of a JWT using a JWK provided as JSON. + * + * @param jwt + * @param jwkJson + * @return + */ + @Throws(IllegalArgumentException::class) + private fun verifyJwtSignature(jwt: String, jwkJson: String): Boolean { + try { + // Parse the JWK from JSON + val jwk = ECKey.parse(jwkJson) + + // Create a JWS object from the JWT string + val jwsObject = JWSObject.parse(jwt) + + // Create a JWS verifier with the EC key + val verifier = ECDSAVerifier(jwk) + + // Verify the JWS signature + return jwsObject.verify(verifier) + } catch (e: Exception) { + // Handle exceptions appropriately + e.printStackTrace() + throw IllegalArgumentException("Invalid signature") + } + } + + + /** + * Processes a JWK from a JWKS (JSON Web Key Set) URI. + * + * @param kid + * @param jwksUri + * @return + */ + private suspend fun processJWKFromJwksUri(kid: String?, jwksUri:String?): JWK? { + if (jwksUri != null) { + val jwkKey = fetchJwks(jwksUri =jwksUri, kid = kid) + return convertToJWK(jwkKey) + } + return null + } + + + /** + * Converts a JwkKey object to a JWK (JSON Web Key). + * + * @param jwkKey + * @return + */ + private fun convertToJWK(jwkKey: JwkKey?): JWK? { + return jwkKey?.let { + ECKey.Builder(Curve.P_256, Base64URL.from(it.x), Base64URL.from(it.y)) + .keyID(it.kid) + .build() + } + } + + + /** + * Fetches a JwkKey object from a specified JWKS (JSON Web Key Set) URI. + * + * @param jwksUri + * @param kid + * @return + */ + private suspend fun fetchJwks(jwksUri: String, kid: String?): JwkKey? { + return withContext(Dispatchers.IO) { + try { + val url = URL(jwksUri) + val json = url.readText() + // Parse JSON into JwksResponse object + val jwksResponse = Gson().fromJson(json, JwksResponse::class.java) + + // Find the JWK with "use" = "sig" + var jwkKey = jwksResponse.keys.firstOrNull { it.use == "sig" } + + // If no "sig" key is found, find by kid + if (jwkKey == null && kid != null) { + jwkKey = jwksResponse.keys.firstOrNull { it.kid == kid } + } + return@withContext jwkKey + } catch (e: Exception) { + println(e.toString()) + return@withContext null + } + } + } +} \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt index 37c4c2e..15cb61e 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDService.kt @@ -8,6 +8,7 @@ import com.nimbusds.jose.jwk.JWK import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator import com.nimbusds.jose.util.Base64URL +import com.nimbusds.jose.util.JSONObjectUtils import java.nio.charset.StandardCharsets import java.security.KeyFactory import java.security.KeyPair @@ -204,6 +205,41 @@ class DIDService : DIDServiceInterface { return encoded } + /** + * Converts a DID string to a JWK (JSON Web Key). + * @param did - Decentralized Identifier (DID) string + * @return JWK object + * @throws IllegalArgumentException if the DID format is invalid, decoding fails, or JSON parsing errors occur + */ + override fun convertDIDToJWK(did: String): JWK { + val multiCodecBytes = try { + Base58.decode(did) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Base58 decoding failed", e) + } + + // Check the length of the decoded bytes + if (multiCodecBytes.size <= 3) { + throw IllegalArgumentException("Decoded bytes are too short to contain valid JSON") + } + + // Decode JSON content + val compactJson = + String(multiCodecBytes.copyOfRange(3, multiCodecBytes.size), StandardCharsets.UTF_8) + + // Parse JSON to retrieve x and y values + val jsonObject = JSONObjectUtils.parse(compactJson) + val x = jsonObject.get("x") as String + val y = jsonObject.get("y") as String + + // Create ECKey using Curve.P_256 (or appropriate curve) + val ecKey = ECKey.Builder(Curve.P_256, Base64URL.from(x), Base64URL.from(y)) + .build() + + // Return as JWK + return ecKey + } + /** * Convert the PrivateKey to ECPrivateKey * @param privateKey diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt index 7963283..b4a664c 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/did/DIDServiceInterface.kt @@ -77,4 +77,12 @@ interface DIDServiceInterface { * @return DID */ fun createEdDSADID(privateKeyX: Base64URL): String + + /** + * Converts a DID string to a JWK (JSON Web Key). + * @param did - Decentralized Identifier (DID) string + * @return JWK object + * @throws IllegalArgumentException if the DID format is invalid or conversion fails + */ + fun convertDIDToJWK(did:String):JWK } \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt new file mode 100644 index 0000000..065e249 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/ExpiryException.kt @@ -0,0 +1,3 @@ +package com.ewc.eudi_wallet_oidc_android.services.exceptions + +class ExpiryException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt new file mode 100644 index 0000000..e947a62 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/exceptions/SignatureException.kt @@ -0,0 +1,3 @@ +package com.ewc.eudi_wallet_oidc_android.services.exceptions + +class SignatureException(message: String, cause: Throwable? = null) : IllegalArgumentException(message, cause) \ No newline at end of file From 4b85c60fb66e9498003c70c6ffaa7a8a52d532e8 Mon Sep 17 00:00:00 2001 From: milan Date: Wed, 24 Jul 2024 01:35:11 +0530 Subject: [PATCH 25/29] Fix #44: Signature validation support of DID starting with did:ebsi --- .../models/DIDDocument.kt | 54 ++++++++++ .../SignatureValidator.kt | 101 ++++++++++++++++-- .../services/network/ApiServices.kt | 4 + 3 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt new file mode 100644 index 0000000..2d7c653 --- /dev/null +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/DIDDocument.kt @@ -0,0 +1,54 @@ +package com.ewc.eudi_wallet_oidc_android.models + +import com.google.gson.annotations.SerializedName + +data class DIDDocument( + @SerializedName("@context") + val context: List, + + @SerializedName("id") + val id: String, + + @SerializedName("controller") + val controller: List, + + @SerializedName("verificationMethod") + val verificationMethods: List, + + @SerializedName("authentication") + val authentication: List, + + @SerializedName("assertionMethod") + val assertionMethods: List, + + @SerializedName("capabilityInvocation") + val capabilityInvocations: List +) + +data class VerificationMethod( + @SerializedName("id") + val id: String, + + @SerializedName("type") + val type: String, + + @SerializedName("controller") + val controller: String, + + @SerializedName("publicKeyJwk") + val publicKeyJwk: PublicKeyJwk +) + +data class PublicKeyJwk( + @SerializedName("kty") + val kty: String, + + @SerializedName("crv") + val crv: String, + + @SerializedName("x") + val x: String, + + @SerializedName("y") + val y: String +) \ No newline at end of file diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt index 380b0e3..777e4ee 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/SignatureValidator.kt @@ -1,9 +1,11 @@ package com.ewc.eudi_wallet_oidc_android.services.credentialValidation +import com.ewc.eudi_wallet_oidc_android.models.DIDDocument import com.ewc.eudi_wallet_oidc_android.models.JwkKey import com.ewc.eudi_wallet_oidc_android.models.JwksResponse import com.ewc.eudi_wallet_oidc_android.services.exceptions.SignatureException import com.ewc.eudi_wallet_oidc_android.services.did.DIDService +import com.ewc.eudi_wallet_oidc_android.services.network.ApiManager import com.google.gson.Gson import com.nimbusds.jose.JWSObject import com.nimbusds.jose.crypto.ECDSAVerifier @@ -14,6 +16,13 @@ import com.nimbusds.jose.util.Base64URL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.net.URL +import com.google.gson.JsonArray +import com.google.gson.JsonParser +import com.nimbusds.jose.JOSEException +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.JWSVerifier +import retrofit2.Response +import java.text.ParseException class SignatureValidator { @@ -34,10 +43,14 @@ class SignatureValidator { val jwsObject = JWSObject.parse(jwt) val header = jwsObject.header val kid = header.keyID + // Check the format of kid and process accordingly - val response = if (kid.startsWith("did:key:z")) { + val response = if ( kid !=null && kid.startsWith("did:key:z")) { processJWKFromKID(kid) - } else { + } else if ( kid !=null && kid.startsWith("did:ebsi:z")){ + processEbsiJWKFromKID(kid) + } + else { processJWKFromJwksUri(kid,jwksUri) } if (response != null) { @@ -52,6 +65,64 @@ class SignatureValidator { } } + // This function fetches and processes the DID Document, and extracts the P-256 JWK if present. + private suspend fun processEbsiJWKFromKID(did: String?): ECKey? { + return try { + // Validate DID format + if (did == null || !did.startsWith("did:ebsi:z")) { + throw IllegalArgumentException("Invalid DID format") + } + + // Fetch the DID document from the API + val response:Response = ApiManager.api.getService()?.ebsiDIDResolver( + "https://api-conformance.ebsi.eu/did-registry/v5/identifiers/$did" + ) ?: throw IllegalStateException("API service not available") + + // Extract the P-256 JWK from the JSON response + val didDocument = response.body() ?: throw IllegalStateException("Empty response body") + extractJWK(didDocument) + } catch (e: Exception) { + // Handle errors, possibly log or rethrow as needed + println("Error processing DID: ${e.message}") + null + } + } + private fun extractJWK(didDocument: DIDDocument): ECKey? { + return try { + // Iterate through each verification method + for (method in didDocument.verificationMethods) { + try { + val publicKeyJwk = method.publicKeyJwk + + // Check if 'crv' is 'P-256' + if (publicKeyJwk.crv == "P-256") { + // Convert the JSON JWK to a Nimbus JWK + val jwk = JWK.parse( + """{ + "kty": "${publicKeyJwk.kty}", + "crv": "${publicKeyJwk.crv}", + "x": "${publicKeyJwk.x}", + "y": "${publicKeyJwk.y}" + }""" + ) + if (jwk is ECKey) { + return jwk + } + } + } catch (e: ParseException) { + // Handle JWK parsing exceptions + println("Error parsing JWK: ${e.message}") + } + } + + // Return null if no matching JWK is found + null + } catch (e: Exception) { + // Handle any unexpected exceptions + println("Error processing DID document: ${e.message}") + null + } + } /** * Processes a JWK from a DID @@ -100,8 +171,17 @@ class SignatureValidator { val jwsObject = JWSObject.parse(jwt) // Create a JWS verifier with the EC key - val verifier = ECDSAVerifier(jwk) +// val verifier = ECDSAVerifier(jwk) + // Get the algorithm from the JWS header + val algorithm = jwsObject.header.algorithm + // Create the appropriate verifier based on the algorithm + val verifier: JWSVerifier = when (algorithm) { + JWSAlgorithm.ES256 -> ECDSAVerifier(jwk.toECKey()) + JWSAlgorithm.ES384 -> ECDSAVerifier(jwk.toECKey()) + JWSAlgorithm.ES512 -> ECDSAVerifier(jwk.toECKey()) + else -> throw JOSEException("Unsupported JWS algorithm $algorithm") + } // Verify the JWS signature return jwsObject.verify(verifier) } catch (e: Exception) { @@ -127,22 +207,27 @@ class SignatureValidator { return null } - /** * Converts a JwkKey object to a JWK (JSON Web Key). * - * @param jwkKey - * @return + * @param jwkKey The JwkKey object. + * @return The JWK object or null if jwkKey is null. */ private fun convertToJWK(jwkKey: JwkKey?): JWK? { return jwkKey?.let { - ECKey.Builder(Curve.P_256, Base64URL.from(it.x), Base64URL.from(it.y)) + val curve = when (it.crv) { + "P-256" -> Curve.P_256 + "P-384" -> Curve.P_384 + "P-521" -> Curve.P_521 + else -> throw IllegalArgumentException("Unsupported curve: ${it.crv}") + } + + ECKey.Builder(curve, Base64URL.from(it.x), Base64URL.from(it.y)) .keyID(it.kid) .build() } } - /** * Fetches a JwkKey object from a specified JWKS (JSON Web Key Set) URI. * diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt index 7c7b488..9b0e3e9 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/network/ApiServices.kt @@ -4,6 +4,7 @@ import com.ewc.eudi_wallet_oidc_android.models.AuthorisationServerWellKnownConfi import com.ewc.eudi_wallet_oidc_android.models.CredentialOffer import com.ewc.eudi_wallet_oidc_android.models.CredentialRequest import com.ewc.eudi_wallet_oidc_android.models.CredentialResponse +import com.ewc.eudi_wallet_oidc_android.models.DIDDocument import com.ewc.eudi_wallet_oidc_android.models.IssuerWellKnownConfiguration import com.ewc.eudi_wallet_oidc_android.models.TokenResponse import okhttp3.ResponseBody @@ -74,4 +75,7 @@ interface ApiService { @Url url: String, @FieldMap map: Map ): Response + + @GET + suspend fun ebsiDIDResolver(@Url url: String): Response } \ No newline at end of file From f8d1fed80f2eeca9a5a9b69345ad35b672fed789 Mon Sep 17 00:00:00 2001 From: milan Date: Wed, 24 Jul 2024 01:36:22 +0530 Subject: [PATCH 26/29] Fix #45: Added extra checking for expiry field in JWT --- .../services/credentialValidation/ExpiryValidator.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt index 4fee784..4578bb9 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/credentialValidation/ExpiryValidator.kt @@ -20,7 +20,13 @@ class ExpiryValidator { return try { val signedJWT = SignedJWT.parse(jwt) val expirationTime = signedJWT.jwtClaimsSet.expirationTime - expirationTime?.before(Date()) ?: throw ExpiryException("JWT token expired") + + // return if expiry not present in the JWT + if (expirationTime == null){ + return false + } + expirationTime.before(Date()) ?: throw ExpiryException("JWT token expired") + } catch (e: ParseException) { throw ExpiryException("JWT token expired", e) } From 4dba4baf9ad3c9f2b3a7d3fd63e547366b16c769 Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 29 Jul 2024 20:34:34 +0530 Subject: [PATCH 27/29] Fix #46: Handle error in process authorisation request --- .../services/issue/IssueService.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index 41e307c..b1d5c33 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -203,7 +203,9 @@ class IssueService : IssueServiceInterface { "issuer_state" to (credentialOffer?.grants?.authorizationCode?.issuerState ?: "") ), ) - + if (response?.code() == 502) { + throw Exception("Unexpected error. Please try again.") + } val location: String? = if (response?.code() == 302) { if (response.headers()["Location"]?.contains("error") == true || response.headers()["Location"]?.contains("error_description") == true) { response.headers()["Location"] @@ -214,9 +216,9 @@ class IssueService : IssueServiceInterface { null } - return if(Uri.parse(location).getQueryParameter("error") != null) { + return if(location != null && Uri.parse(location).getQueryParameter("error") != null) { location - }else if (Uri.parse(location).getQueryParameter("code") != null + }else if (location != null && Uri.parse(location).getQueryParameter("code") != null || Uri.parse(location).getQueryParameter("presentation_definition") != null ) { location From 76b78d92ed08f6fe72a1d7388ee08ae84b8a916b Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 29 Jul 2024 21:40:17 +0530 Subject: [PATCH 28/29] Fix #48 - Support value by reference in dynamic credential request --- .../models/PresentationDefinition.kt | 1 + .../eudi_wallet_oidc_android/services/issue/IssueService.kt | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationDefinition.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationDefinition.kt index 68ecd15..9a58d4f 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationDefinition.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/models/PresentationDefinition.kt @@ -5,6 +5,7 @@ import com.google.gson.annotations.SerializedName data class PresentationDefinition( @SerializedName("id") var id: String? = null, + @SerializedName("name") var name: String? = null, @SerializedName("format") var format: Map? = mapOf(), @SerializedName("input_descriptors") var inputDescriptors: ArrayList? = arrayListOf() diff --git a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt index b1d5c33..c12792a 100644 --- a/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt +++ b/eudi-wallet-oidc-android/src/main/java/com/ewc/eudi_wallet_oidc_android/services/issue/IssueService.kt @@ -216,10 +216,15 @@ class IssueService : IssueServiceInterface { null } + + return if(location != null && Uri.parse(location).getQueryParameter("error") != null) { location }else if (location != null && Uri.parse(location).getQueryParameter("code") != null || Uri.parse(location).getQueryParameter("presentation_definition") != null + || (Uri.parse(location).getQueryParameter("request_uri") != null && + Uri.parse(location).getQueryParameter("response_type") == null && + Uri.parse(location).getQueryParameter("state") == null) ) { location } else { From a4f403c96fa656ef79eaae1b6480edefcad3de6f Mon Sep 17 00:00:00 2001 From: milan Date: Mon, 29 Jul 2024 21:44:34 +0530 Subject: [PATCH 29/29] Fix #49 - filtering JWT and SD JWT credentials --- .../services/verification/VerificationService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c638d37..8e8ace2 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 @@ -325,7 +325,7 @@ class VerificationService : VerificationServiceInterface { for (item in allCredentials) { if (isSdJwt && item?.contains("~") == true) filteredCredentials.add(item) - else if (!isSdJwt && item?.contains("~") != null) + else if (!isSdJwt && item?.contains("~") == false) filteredCredentials.add(item) } return filteredCredentials